Skip to content

Comparing Mach-O files can fail due to padding or code signature differences

The App Fair Project enables anyone to create an open-source iOS/macOS app from a Swift template by forking https://github.com/appfair/App.git. Tagged releases to this fork will automatically build and upload a release binary using GitHub actions. When the developer subsequently creates a pull request from their App fork back to the base repository, another GitHub action will validate the app and publish it through various channels, such as TestFlight and AltStore.

Since the developer controls the release artifacts in their fork, and could thereby subvert the build process or inject malware into the released executables, the artifacts published in these forks of appfair/App.git are deemed "untrusted". Therefore, in order to establish trust with the candidate app, the base repository will re-build the entire app from the pull request in its own trusted environment. Iff the two resulting executables match sufficiently, the forked binary will be deemed verified and trusted, and therefore will be added (with its sha256 hashes) to the published catalog of apps. This guarantees that all verified apps have their complete source code available for public scrutiny.

I would like to use diffoscope for comparing the executable Mach-O binaries instead of the cobbled-together tools that I've assembled, but running it in the unzipped folder from appfair_reproducible.zip yields:

diffoscope --exclude-directory-metadata=recursive */*.ipa

--- app1/Encyclopedia-Galactica-iOS.ipa
+++ app2/Encyclopedia-Galactica-iOS.ipa
├── Payload/Encyclopedia Galactica.app/Encyclopedia Galactica
│ ├── arm64
│ │┄ Format-specific differences are supported for this file format but no file-specific differences were detected; falling back to a binary diff. file(1) reports: Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>
│ │ @@ -4133,36 +4133,36 @@
│ │  00010240: 41f6 5c9b 6ca8 7bc4 2344 ec9e 5b0a 25bb  A.\.l.{.#D..[.%.
│ │  00010250: 2797 ba2f f6eb 673c ec7b 5279 f8b6 83a2  '../..g<.{Ry....

<etc…>

├── Payload/Encyclopedia Galactica.app/Frameworks/App.framework/App
│ ├── strings -a -n 8 {}
│ │ @@ -10176,292 +10176,291 @@
│ │  __hidden#8836_
│ │  __hidden#8837_
│ │ │ +001bb460  3e 78 fc ff f3 ff ff ff  b4 78 fc ff f9 ff ff ff  |>x.......x......|
│ │ │  001bb470  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

<etc…>

One of the issues that makes the two binaries distinct is that even through they are compiled within similar GitHub actions environments (and therefore the exact same Xcode/llvm toolchain versions), they will wind up having different Ad-Hoc code signatures (which is a requirement for being able to run anything on Apple's arm64 devices). Stripping out the code signatures can help, or else examining the underlying assembly of the code. In my case, I diff the output of otool -tVX -function_offsets Payload/*.app/Frameworks/App.framework/App for each of the frameworks and apps. This works 90% of the time, but sometimes there seems to be additional padding that is inserted in the library, as can be seem from diffing the output of the otool command:

diff --color=always */app.S
4c4
<    +12 	b	0x167e04 ; symbol stub for: _swift_deallocClassInstance
---
>    +12 	b	0x167e94 ; symbol stub for: _swift_deallocClassInstance
7,8c7,8
<    +24 	bl	0x5f48
<    +28 	adrp	x8, 438 ; 0x1bb000
---
>    +24 	bl	0x6e34
>    +28 	adrp	x8, 437 ; 0x1bb000
21c21
<    +80 	bl	0x165920
---
>    +80 	bl	0x1659b0
26c26
<   +100 	adrp	x16, 413 ; 0x1a2000
---
>   +100 	adrp	x16, 412 ; 0x1a2000
34c34
<   +132 	adrp	x0, 430 ; 0x1b3000
---
>   +132 	adrp	x0, 429 ; 0x1b3000
40c40
<   +156 	adrp	x16, 413 ; 0x1a2000
---
>   +156 	adrp	x16, 412 ; 0x1a2000

<etc…>

359835c358916
<   +776 	b.ne	0x165370
---
>   +776 	b.ne	0x165400
359844c358925
<   +812 	bl	0x167ac8 ; symbol stub for: ___stack_chk_fail
---
>   +812 	bl	0x167b58 ; symbol stub for: ___stack_chk_fail

It appears that the contents of the instructions are all the same, but the offsets of the various instructions are all moved around.

So to summarize: I'd like it if diffomatic could intelligently parse these Macho-O file and compare their underlying instructions.

One possible solution might be to strip each binary of its code signature (using the codesign --remove-signature command) and then somehow "compact" the resulting Mach-O file (if such a tool exists; maybe something like lipo -segalign), and then lastly comparing the assembly dump of the two files.

P.S. This issue is similar to “Add option to allow differences in specific areas” (#262), and is also related to the issues linked to from “ELF file diffs doesnt handle offets well” (#308).

Edited by Marc Prud'hommeaux
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information