[GH-ISSUE #5542] [Bug] FAT target admission trusts CPU declarations that contradict thin Mach-O headers #3374

Closed
opened 2026-10-31 00:39:40 +00:00 by bsakaguchi · 4 comments
Owner

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

Before submitting

  • Searched open and closed issues and related PRs, including #656, #713, #5462 and #5501.
  • This report uses synthetic bytes and contains no credentials or private target data.

Area

Native analysis or decompilation

Observed behavior

A controlled source-boundary probe accepts a single-entry FAT Mach-O as arm64 even though its embedded thin header identifies x86_64. Both parseExecutableHeader and file-backed parseBinaryTarget return arm64; readMachoImage rejects the same bytes for CPU type/subtype disagreement. The production GhidraProvider.inspectTargetSupport seam reports supported for that target. Ghidra itself was not launched.

Reproduction

At source revision 0c440bfc07f3cb343e2e836d27cccaed539cfe9c, a Node/tsx probe constructed a 64-byte FAT32 fixture: big-endian magic 0xcafebabe, one table entry with CPU 0x0100000c (arm64), subtype 0, offset 32, size 32, alignment 0. At offset 32, the little-endian 64-bit Mach-O header has magic 0xfeedfacf, CPU 0x01000007 (x86_64), subtype 3, and zero remaining fields (including filetype). SHA-256: 71a6c2eb1e53119f4d943fbde000379bd4394e7315e2bc054a0b5e1d1f1f770f.

The probe called parseExecutableHeader(bytes, "arm64"), parseBinaryTarget(path, {hostArchitecture: "arm64"}), the shared Mach-O reader over the same bytes, and GhidraProvider.inspectTargetSupport over the admitted target. The mismatch reproduced on a clean detached worktree at that exact revision; an earlier dirty-snapshot result was independently replayed rather than treated as clean-baseline evidence.

Expected behavior

Validate FAT CPU type/subtype against parsed thin-header facts before publishing target architecture metadata or selecting a slice. The existing validateFatSliceIdentity already enforces this rule for the Mach-O reader. Preserve incomplete header facts as unknown and acquire bounded headers for every relevant declaration.

Impact and scope

Observed: provider-neutral target identity contradicts the same file's embedded CPU identity, and the Ghidra support boundary accepts it. Actual engine output and behavior on other providers are unverified. This is a malformed synthetic single-entry container; frequency and real-world incidence are unknown.

REA version

Source 0c440bfc07f3cb343e2e836d27cccaed539cfe9c.

Runtime and platform

Node 24.18.0, macOS 26.3 arm64. No external analysis engine run.

Target and artifact context

Synthetic inert FAT32 containing one x86_64 Mach-O header under an arm64 FAT declaration.

Interface

Other: direct production TypeScript boundaries, including the provider support seam.

Earliest failure stage

Setup or provider admission

Relevant output or evidence

parseExecutableHeader: ok=true, format=mach-o-universal, architecture=arm64,
  availableArchitectures=[arm64], universalSlice={offset:32,size:32}
parseBinaryTarget: same architecture and slice facts
readMachoImage: status=malformed,
  reason="FAT architecture 0 CPU type/subtype disagrees with its Mach-O header"
GhidraProvider.inspectTargetSupport: status=supported

Source owners at the pinned revision: target admission, existing slice validator consumer, Ghidra multi-architecture guard.

Typed outcome or recovery expected

A malformed-target result explaining the declaration/thin-header identity disagreement, before any provider import.

Narrow follow-up to #656 and merged #713: the guard refuses multi-architecture universal targets, while this malformed one-entry table claims only arm64. This report does not claim a new live Ghidra wrong-output reproduction. #5462 concerns archive payloads under FAT declarations; #5501 concerns valid foreign-only Mach-O admission. Neither covers declaration/thin-header CPU disagreement.

Originally created by @morluto on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5542 ### Before submitting - [x] Searched open and closed issues and related PRs, including #656, #713, #5462 and #5501. - [x] This report uses synthetic bytes and contains no credentials or private target data. ### Area Native analysis or decompilation ### Observed behavior A controlled source-boundary probe accepts a single-entry FAT Mach-O as arm64 even though its embedded thin header identifies x86_64. Both `parseExecutableHeader` and file-backed `parseBinaryTarget` return arm64; `readMachoImage` rejects the same bytes for CPU type/subtype disagreement. The production `GhidraProvider.inspectTargetSupport` seam reports supported for that target. Ghidra itself was not launched. ### Reproduction At source revision `0c440bfc07f3cb343e2e836d27cccaed539cfe9c`, a Node/tsx probe constructed a 64-byte FAT32 fixture: big-endian magic `0xcafebabe`, one table entry with CPU `0x0100000c` (arm64), subtype 0, offset 32, size 32, alignment 0. At offset 32, the little-endian 64-bit Mach-O header has magic `0xfeedfacf`, CPU `0x01000007` (x86_64), subtype 3, and zero remaining fields (including filetype). SHA-256: `71a6c2eb1e53119f4d943fbde000379bd4394e7315e2bc054a0b5e1d1f1f770f`. The probe called `parseExecutableHeader(bytes, "arm64")`, `parseBinaryTarget(path, {hostArchitecture: "arm64"})`, the shared Mach-O reader over the same bytes, and `GhidraProvider.inspectTargetSupport` over the admitted target. The mismatch reproduced on a clean detached worktree at that exact revision; an earlier dirty-snapshot result was independently replayed rather than treated as clean-baseline evidence. ### Expected behavior Validate FAT CPU type/subtype against parsed thin-header facts before publishing target architecture metadata or selecting a slice. The existing `validateFatSliceIdentity` already enforces this rule for the Mach-O reader. Preserve incomplete header facts as unknown and acquire bounded headers for every relevant declaration. ### Impact and scope Observed: provider-neutral target identity contradicts the same file's embedded CPU identity, and the Ghidra support boundary accepts it. Actual engine output and behavior on other providers are unverified. This is a malformed synthetic single-entry container; frequency and real-world incidence are unknown. ### REA version Source `0c440bfc07f3cb343e2e836d27cccaed539cfe9c`. ### Runtime and platform Node 24.18.0, macOS 26.3 arm64. No external analysis engine run. ### Target and artifact context Synthetic inert FAT32 containing one x86_64 Mach-O header under an arm64 FAT declaration. ### Interface Other: direct production TypeScript boundaries, including the provider support seam. ### Earliest failure stage Setup or provider admission ### Relevant output or evidence ```text parseExecutableHeader: ok=true, format=mach-o-universal, architecture=arm64, availableArchitectures=[arm64], universalSlice={offset:32,size:32} parseBinaryTarget: same architecture and slice facts readMachoImage: status=malformed, reason="FAT architecture 0 CPU type/subtype disagrees with its Mach-O header" GhidraProvider.inspectTargetSupport: status=supported ``` Source owners at the pinned revision: [target admission](https://github.com/morluto/rea/blob/0c440bfc07f3cb343e2e836d27cccaed539cfe9c/src/domain/binaryTarget.ts#L130), [existing slice validator consumer](https://github.com/morluto/rea/blob/0c440bfc07f3cb343e2e836d27cccaed539cfe9c/src/artifacts/apple/MachoLoadCommandReader.ts#L239), [Ghidra multi-architecture guard](https://github.com/morluto/rea/blob/0c440bfc07f3cb343e2e836d27cccaed539cfe9c/src/ghidra/GhidraProvider.ts#L264). ### Typed outcome or recovery expected A malformed-target result explaining the declaration/thin-header identity disagreement, before any provider import. ### Analysis and related work Narrow follow-up to #656 and merged #713: the guard refuses multi-architecture universal targets, while this malformed one-entry table claims only arm64. This report does not claim a new live Ghidra wrong-output reproduction. #5462 concerns archive payloads under FAT declarations; #5501 concerns valid foreign-only Mach-O admission. Neither covers declaration/thin-header CPU disagreement.
bsakaguchi 2026-10-31 00:39:40 +00:00
  • closed this issue
  • added the
    triage
    bug
    labels
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #5462 ([Bug] Native inspection admits universal static archives as Mach-O images). Both address universal (FAT) container validation, CPU declaration alignment with thin Mach-O slice headers, and architecture admission boundaries. Cataloged under Cluster BE in consolidation tracker #4330.

<!-- gh-comment-id:6295548366 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #5462 (`[Bug] Native inspection admits universal static archives as Mach-O images`). Both address universal (FAT) container validation, CPU declaration alignment with thin Mach-O slice headers, and architecture admission boundaries. Cataloged under Cluster BE in consolidation tracker #4330.
Author
Owner

@CassieFox23 commented on GitHub (Oct 20, 2026):

TL;DR: This looks fixed on main for the command line. A universal file whose CPU label contradicts its Mach-O header is now refused. Closing is the maintainers' call.

I checked main 1629266e3 (Linux, Node 24.18.0). The fixes are 497037e6e and 44521d3d7.

I rebuilt the 64-byte file from the issue. Its sha256 matches the one listed there. The universal header says arm64. The Mach-O header at offset 32 says x86_64. Both inspect-macho and list-architectures exit 1. Both say "FAT architecture 0 CPU type/subtype disagrees with its Mach-O header". The issue's own call, parseExecutableHeader(bytes, "arm64"), now returns ok: false with the same reason. It used to return arm64. The same file with a matching x86_64 label passes this gate. Then it stops, because the Mach-O provider needs macOS.

Not run

  • parseBinaryTarget called directly.
  • The Ghidra support check from the issue.
  • A mismatch in the subtype only.
  • macOS.

Thanks, morluto, for the clear report.

Commands
rea inspect-macho fat-contradict.bin --json
rea list-architectures fat-contradict.bin --json

Both exit 1: {"error":"Analysis failed","code":"target_unavailable","category":"unsupported_target","message":"Cannot open artifact: FAT architecture 0 CPU type/subtype disagrees with its Mach-O header.","details":{"reason":"FAT architecture 0 CPU type/subtype disagrees with its Mach-O header","constraint":"file_format_rejected"}}

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.11
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0
  • Head: 1629266e3

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6299403411 --> @CassieFox23 commented on GitHub (Oct 20, 2026): **TL;DR:** This looks fixed on main for the command line. A universal file whose CPU label contradicts its Mach-O header is now refused. Closing is the maintainers' call. I checked main 1629266e3 (Linux, Node 24.18.0). The fixes are 497037e6e and 44521d3d7. I rebuilt the 64-byte file from the issue. Its sha256 matches the one listed there. The universal header says arm64. The Mach-O header at offset 32 says x86_64. Both `inspect-macho` and `list-architectures` exit 1. Both say "FAT architecture 0 CPU type/subtype disagrees with its Mach-O header". The issue's own call, `parseExecutableHeader(bytes, "arm64")`, now returns `ok: false` with the same reason. It used to return arm64. The same file with a matching x86_64 label passes this gate. Then it stops, because the Mach-O provider needs macOS. ### Not run - `parseBinaryTarget` called directly. - The Ghidra support check from the issue. - A mismatch in the subtype only. - macOS. Thanks, morluto, for the clear report. <details> <summary>Commands</summary> ```sh rea inspect-macho fat-contradict.bin --json rea list-architectures fat-contradict.bin --json ``` Both exit 1: `{"error":"Analysis failed","code":"target_unavailable","category":"unsupported_target","message":"Cannot open artifact: FAT architecture 0 CPU type/subtype disagrees with its Mach-O header.","details":{"reason":"FAT architecture 0 CPU type/subtype disagrees with its Mach-O header","constraint":"file_format_rejected"}}` </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.11 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 24.18.0 - Head: 1629266e3 </details> 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

@Shreyash021104 commented on GitHub (Oct 21, 2026):

Covered the paths the earlier retest left unrun, on main a5f982d (Linux x64, Node 24.18.0). I called parseExecutableHeader and the file-backed parseBinaryTarget directly with hostArchitecture set to arm64 and to x64:

FAT fixture (one or two slices) parseExecutableHeader parseBinaryTarget
Issue case: FAT arm64, thin x86_64 refused refused
Subtype only: FAT arm64/0, thin arm64/2 (arm64e) refused refused
Subtype only: FAT x86_64/3, thin x86_64/8 (Haswell) refused refused
Mismatch only in the slice the host would not select refused (FAT architecture 1 ...) refused
Consistent two-slice control arm64 / x86_64 arm64 / x86_64

Every refusal carries FAT architecture N CPU type/subtype disagrees with its Mach-O header. Since parseBinaryTarget refuses these files, no BinaryTarget reaches GhidraProvider.inspectTargetSupport. That part is derived from the call order; I did not launch Ghidra.

Regression check with real universal binaries from public packages (fsevents 2.3.2/2.3.3, plus the MarkupSafe, msgpack, charset-normalizer, orjson and regex universal2 wheels): all 8 are still admitted, and in each one the FAT entries and thin headers carry identical CPU type and subtype.

One open question, not claimed as a bug: a slice that differs only in the capability bits (FAT x86_64 subtype 0x3, thin 0x80000003) is also refused. None of the real binaries above has that difference, so I could not establish whether any toolchain produces it.

<!-- gh-comment-id:6310809118 --> @Shreyash021104 commented on GitHub (Oct 21, 2026): Covered the paths the earlier retest left unrun, on `main` `a5f982d` (Linux x64, Node 24.18.0). I called `parseExecutableHeader` and the file-backed `parseBinaryTarget` directly with `hostArchitecture` set to `arm64` and to `x64`: | FAT fixture (one or two slices) | `parseExecutableHeader` | `parseBinaryTarget` | |---|---|---| | Issue case: FAT arm64, thin x86_64 | refused | refused | | Subtype only: FAT arm64/0, thin arm64/2 (arm64e) | refused | refused | | Subtype only: FAT x86_64/3, thin x86_64/8 (Haswell) | refused | refused | | Mismatch only in the slice the host would not select | refused (`FAT architecture 1 ...`) | refused | | Consistent two-slice control | arm64 / x86_64 | arm64 / x86_64 | Every refusal carries `FAT architecture N CPU type/subtype disagrees with its Mach-O header`. Since `parseBinaryTarget` refuses these files, no `BinaryTarget` reaches `GhidraProvider.inspectTargetSupport`. That part is derived from the call order; I did not launch Ghidra. Regression check with real universal binaries from public packages (`fsevents` 2.3.2/2.3.3, plus the MarkupSafe, msgpack, charset-normalizer, orjson and regex universal2 wheels): all 8 are still admitted, and in each one the FAT entries and thin headers carry identical CPU type and subtype. One open question, not claimed as a bug: a slice that differs only in the capability bits (FAT x86_64 subtype `0x3`, thin `0x80000003`) is also refused. None of the real binaries above has that difference, so I could not establish whether any toolchain produces it.
Author
Owner

@morluto commented on GitHub (Oct 21, 2026):

Closing as fixed on main by github.com/morluto/rea@44521d3d75, retained at 67a6477f01.

Target admission consumes shared thin-header CPU facts and validates every relevant FAT declaration against the embedded CPU type/subtype before host slice selection. Target/parser regressions cover selected and unselected mismatches with consistent controls. No Ghidra launch was required or performed for this source admission audit.

This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.

<!-- gh-comment-id:6324278707 --> @morluto commented on GitHub (Oct 21, 2026): Closing as fixed on main by https://github.com/morluto/rea/commit/44521d3d752a4638957e062dfc243a5ec0950fba, retained at 67a6477f011ae358fc333e5a6e26c86d932654ed. Target admission consumes shared thin-header CPU facts and validates every relevant FAT declaration against the embedded CPU type/subtype before host slice selection. Target/parser regressions cover selected and unselected mismatches with consistent controls. No Ghidra launch was required or performed for this source admission audit. This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.
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#3374
No description provided.