[GH-ISSUE #592] native: byte-swapped fat headers (FAT_CIGAM / FAT_CIGAM_64) are misreported as unsupported thin Mach-O #296

Closed
opened 2026-10-15 18:23:35 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @morluto on GitHub (Oct 5, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/592

Problem

selectMachoSlice in src/native/AppleDispatchMetadata.ts recognizes only the two big-endian universal-binary magics:

  • FAT_MAGIC (0xcafebabe)
  • FAT_MAGIC_64 (0xcafebabf)

A universal binary whose fat header is stored little-endian is therefore never recognized as a fat container. The decoder falls through to the thin-Mach-O path and reports:

Only little-endian 64-bit Mach-O metadata is supported

That diagnostic is wrong for these inputs. The container really is a fat binary, and the actual limitation is that its header byte order is not decoded.

The unrecognized magics are:

Constant Big-endian read Meaning
FAT_CIGAM 0xbebafeca 32-bit fat header, little-endian byte order
FAT_CIGAM_64 0xbfbafeca 64-bit fat header, little-endian byte order

Both are produced by real toolchains: lipo and ctool -arch all emit byte-swapped fat headers on hosts whose native fat-header write order differs from the reader's. Such a file is a valid universal binary, not a malformed or thin one.

Why this matters

The current message directs the caller toward the wrong conclusion — that the file is not a supported Mach-O — when the true constraint is header byte order. Per the boundary contract guidance in AGENTS.md, a malformed input must stay distinct from an unsupported target and from a real format limitation, and a transformation that loses information should preserve the reported value rather than guessing a canonical identity.

Expected behavior

selectMachoSlice should select the fat-header byte order from the magic:

  • 0xcafebabe / 0xcafebabf — big-endian, strides of 20 and 32
  • 0xbebafeca / 0xbfbafeca — little-endian, strides of 20 and 32

Every subsequent field read (nfat_arch, cputype, cpusubtype, offset, size, align) uses that order consistently. The selected slice is then decoded as a little-endian 64-bit Mach-O regardless of the fat header's byte order, because the slice payload's own magic is validated separately at src/native/AppleDispatchMetadata.ts:36.

A fat header whose byte order is recognized but whose contents are malformed should keep reporting the existing reasons — Malformed FAT architecture table, FAT slice exceeds file, Ambiguous FAT architecture slice, Requested FAT architecture is absent — with the byte order stated so the diagnostic identifies the producer representation that failed.

Scope notes

This is deliberately separate from PR #590, which adds FAT_MAGIC_64 support for the big-endian magics only. That change is correct on its own and should not be held back by this gap.

Suggested coverage, in the style of src/native/AppleDispatchMetadata.test.ts:

  • decode a FAT_CIGAM and a FAT_CIGAM_64 container with a thin little-endian Mach-O payload, asserting the same file_offset values the big-endian fixtures produce
  • reject truncated little-endian architecture tables with the byte order in the message
  • confirm a little-endian fat header wrapping a big-endian (or 32-bit) slice still reports Only little-endian 64-bit Mach-O metadata is supported, so slice validation is not weakened

scripts/verify-apple-dispatch.mjs should gain a lipo-produced universal fixture whose header byte order does not match the host-native big-endian read path, so the support claim rests on a real toolchain rather than a synthesized header.

Verification

npm run check:pr, and npm run verify:apple-dispatch on macOS against a real lipo universal fixture. Format fixtures establish parser behavior only; they do not replace the real-toolchain check for the support claim.

Originally created by @morluto on GitHub (Oct 5, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/592 ## Problem `selectMachoSlice` in `src/native/AppleDispatchMetadata.ts` recognizes only the two big-endian universal-binary magics: - `FAT_MAGIC` (`0xcafebabe`) - `FAT_MAGIC_64` (`0xcafebabf`) A universal binary whose fat header is stored little-endian is therefore never recognized as a fat container. The decoder falls through to the thin-Mach-O path and reports: ``` Only little-endian 64-bit Mach-O metadata is supported ``` That diagnostic is wrong for these inputs. The container really is a fat binary, and the actual limitation is that its header byte order is not decoded. The unrecognized magics are: | Constant | Big-endian read | Meaning | | --- | --- | --- | | `FAT_CIGAM` | `0xbebafeca` | 32-bit fat header, little-endian byte order | | `FAT_CIGAM_64` | `0xbfbafeca` | 64-bit fat header, little-endian byte order | Both are produced by real toolchains: `lipo` and `ctool -arch all` emit byte-swapped fat headers on hosts whose native fat-header write order differs from the reader's. Such a file is a valid universal binary, not a malformed or thin one. ## Why this matters The current message directs the caller toward the wrong conclusion — that the file is not a supported Mach-O — when the true constraint is header byte order. Per the boundary contract guidance in `AGENTS.md`, a malformed input must stay distinct from an unsupported target and from a real format limitation, and a transformation that loses information should preserve the reported value rather than guessing a canonical identity. ## Expected behavior `selectMachoSlice` should select the fat-header byte order from the magic: - `0xcafebabe` / `0xcafebabf` — big-endian, strides of 20 and 32 - `0xbebafeca` / `0xbfbafeca` — little-endian, strides of 20 and 32 Every subsequent field read (`nfat_arch`, `cputype`, `cpusubtype`, `offset`, `size`, `align`) uses that order consistently. The selected slice is then decoded as a little-endian 64-bit Mach-O regardless of the fat header's byte order, because the slice payload's own magic is validated separately at `src/native/AppleDispatchMetadata.ts:36`. A fat header whose byte order is recognized but whose contents are malformed should keep reporting the existing reasons — `Malformed FAT architecture table`, `FAT slice exceeds file`, `Ambiguous FAT architecture slice`, `Requested FAT architecture is absent` — with the byte order stated so the diagnostic identifies the producer representation that failed. ## Scope notes This is deliberately separate from PR #590, which adds `FAT_MAGIC_64` support for the big-endian magics only. That change is correct on its own and should not be held back by this gap. Suggested coverage, in the style of `src/native/AppleDispatchMetadata.test.ts`: - decode a `FAT_CIGAM` and a `FAT_CIGAM_64` container with a thin little-endian Mach-O payload, asserting the same `file_offset` values the big-endian fixtures produce - reject truncated little-endian architecture tables with the byte order in the message - confirm a little-endian fat header wrapping a big-endian (or 32-bit) slice still reports `Only little-endian 64-bit Mach-O metadata is supported`, so slice validation is not weakened `scripts/verify-apple-dispatch.mjs` should gain a `lipo`-produced universal fixture whose header byte order does not match the host-native big-endian read path, so the support claim rests on a real toolchain rather than a synthesized header. ## Verification `npm run check:pr`, and `npm run verify:apple-dispatch` on macOS against a real `lipo` universal fixture. Format fixtures establish parser behavior only; they do not replace the real-toolchain check for the support claim.
bsakaguchi 2026-10-15 18:23:35 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#296
No description provided.