[GH-ISSUE #674] [Bug] inspect-macho fails on universal x86_64 + arm64e Mach-O binaries such as /bin/ls #314

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

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

On an Apple Silicon Mac, rea inspect-macho (MCP inspect_macho, provider native-macos) fails with a generic execution_failure on every universal Mach-O with x86_64 and arm64e slices that we tried: /bin/ls, /bin/cat, /usr/bin/true, /usr/bin/zip, and a clang-built x86_64 + arm64e test program. REA admits the target and selects its arm64 architecture, but the operation returns no evidence. The same command succeeds on a universal x86_64 + arm64 binary, on an arm64e-only binary, and on the arm64e slice thinned out of the failing file. The error tells the caller to retry and run rea doctor, which cannot help because the failure is deterministic.

Observed locally with both tested builds:

hello-x86_64-arm64e exit=1
hello-x86_64-arm64 exit=0
hello-arm64e exit=0
/bin/ls exit=1

lipo -info reports x86_64 arm64e for the failing fixture and x86_64 arm64 for the control. On this macOS build, lipo -info /bin/ls reports x86_64 arm64e arm64e.x1, and /System/Applications/Calculator.app (slices arm64e arm64e.x1, no x86_64) succeeds with exit 0.

The JSON written to stdout for each failing target:

{
  "error": "Analysis failed",
  "code": "execution_failure",
  "category": "execution_failure",
  "message": "Analysis could not complete. Retry once; if it continues, run `rea doctor`.",
  "retryable": false,
  "remediation": {
    "action": "Analysis could not complete. Retry once; if it continues, run `rea doctor`."
  },
  "details": {
    "provider_id": "native-macos",
    "operation": "inspect_macho"
  }
}

With REA_LOG_LEVEL=trace, stderr adds only {"level":50,...,"command":"inspect-macho","status":"error","msg":"CLI command failed"}, so the output does not identify which native command failed. rea list-architectures on the same failing fixture exits 0 and reports both slices.

For the x86_64 + arm64 control, the provenance in the successful result shows the provider passing the selected architecture to the slice-aware tools: otool -h -l -arch arm64, nm -gjU -arch arm64, dyld_info -imports -arch arm64, dyld_info -exports -arch arm64, dwarfdump --uuid --arch=arm64, vtool -show-build -arch arm64. When the native tools are run directly against the failing fixture or /bin/ls, -arch arm64 fails for otool, nm, dyld_info, and vtool:

otool -h -l -arch arm64        -> exit 1  "file: ... does not contain architecture: arm64"
nm -gjU -arch arm64            -> exit 1  "error: file: ... does not contain architecture: arm64"
dyld_info -imports -arch arm64 -> exit 1  "... does not contain specified arch(s)"
vtool -show-build -arch arm64  -> exit 1  "error: ... file does not contain architecture: arm64"
dwarfdump --uuid --arch=arm64  -> exit 0, but prints no UUID

The two failing inputs carry different arm64e ABI variants. lipo -detailed_info reports the /bin/ls slice as arm64e (PTR_AUTH_VERSION USERSPACE 0) and the clang-built slice as arm64e.v1 (PTR_AUTH_VERSION USERSPACE 1). On /bin/ls, otool, nm, and dyld_info all exit 0 with -arch arm64e. On the clang-built fixture, otool and nm exit 0 with -arch arm64e, but dyld_info -imports/-exports -arch arm64e exits 1 ("does not contain specified arch(s)") and exits 0 with -arch arm64e.v1, where nm -arch arm64e.v1 exits 1. All three tools exit 0 with -arch x86_64 on both inputs.

Reproduction

The steps use a two-line C program so the failing input is reproducible without relying on a particular macOS build. Run them on an Apple Silicon Mac with Xcode installed.

WORK="$(mktemp -d)"; cd "$WORK"
printf 'int main(void){return 0;}\n' > hello.c
xcrun clang -arch x86_64 -arch arm64e -o hello-x86_64-arm64e hello.c   # failing input
xcrun clang -arch x86_64 -arch arm64  -o hello-x86_64-arm64  hello.c   # control
lipo hello-x86_64-arm64e -thin arm64e -output hello-arm64e              # control
lipo -info hello-x86_64-arm64e hello-x86_64-arm64

for t in hello-x86_64-arm64e hello-x86_64-arm64 hello-arm64e /bin/ls; do
  HOME="$WORK" npx -y rea-agents@4.0.1 inspect-macho "$t" --json > out.json 2>/dev/null
  echo "$t exit=$?"
done

Optionally, compare the native tools that the provider runs with an explicit architecture:

xcrun otool -h -l -arch arm64 hello-x86_64-arm64e > /dev/null; echo "otool -arch arm64 exit=$?"
xcrun otool -h -l -arch arm64e hello-x86_64-arm64e > /dev/null; echo "otool -arch arm64e exit=$?"

Expected behavior

inspect_macho is documented as "Inspect Mach-O slices, load commands, imports, exports, dependencies, build metadata, segments, sections, permissions, and exact command provenance without launching Hopper" (src/contracts/nativeToolContracts.ts). REA admits the universal x86_64 + arm64e file as a supported mach-o target with host-compatible architecture arm64, the same way it admits a universal x86_64 + arm64 file, which it inspects successfully. Since #601, the universal x86_64 + arm64 case returns evidence for the selected arm64 slice (its normalized_result.uuid matches that slice) while architectures lists both slices. The x86_64 + arm64e file should give the same kind of result: exit 0, evidence for the slice REA selected for the arm64 host (the arm64e slice), and both slices listed in architectures.

Impact and scope

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: On Apple Silicon, inspect_macho fails on every universal x86_64 + arm64e Mach-O we tried. This class includes /bin/ls and other macOS system binaries that ship x86_64 and arm64e slices. The failure is deterministic. On the same inputs list-architectures keeps working, as does inspect-signature on /bin/ls. arm64e-only targets such as Calculator.app are unaffected, and no data is lost.
  • Workaround status: available
  • Workaround basis: lipo <file> -thin arm64e -output <copy> produces a thin copy that inspect-macho inspects successfully (verified locally). The resulting evidence names the copy's digest rather than the original file's, and the inventory loses the x86_64 slice, so the workaround preserves the slice data but not the original artifact identity.

REA version

REA: rea-agents 4.0.1 from npm (latest dist-tag; release tag rea-agents-4.0.1, commit feeb19b8b3172b2827b80fde8f09570ce5aeed88), and a source build of main at 19f8cddfd6f283188567b8ff1e49b810d79952ee. Both builds behave identically.

Runtime and platform

Node.js v24.21.0; macOS 27.0.1 (26A434) on Apple Silicon (arm64); Xcode 27.0 (27A266a), Apple clang 21.0.0.

Target and artifact context

Universal (FAT) Mach-O executables with x86_64 and arm64e slices: macOS system binaries such as /bin/ls (x86_64 arm64e arm64e.x1) and a clang-built x86_64 arm64e.v1 test program.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: inspect_macho tool contract in src/contracts/nativeToolContracts.ts, target admission in src/domain/binaryTarget.ts, which accepts the FAT file with host architecture arm64, and the successful x86_64 + arm64 control described under #601 and observed here.
  • Failure source: local CLI runs of inspect-macho on a clang-built universal x86_64 + arm64e fixture and on /bin/ls, with both tested builds, shown under Observed behavior.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Typed outcome or recovery expected

With the universal x86_64 + arm64e fixture built in Reproduction, rea inspect-macho hello-x86_64-arm64e --json currently exits 1 with execution_failure. It passes when the command exits 0, normalized_result.uuid equals the arm64e UUID that xcrun dwarfdump --uuid hello-x86_64-arm64e prints, and normalized_result.architectures.items names both slices. A UUID equal to the x86_64 slice's fails the check, because an x86_64 result would hide the defect. /bin/ls on Apple Silicon must also exit 0, report the UUID that xcrun dwarfdump --uuid /bin/ls labels (arm64e) (not the arm64e.x1 or x86_64 UUID) on a host that runs the plain arm64e slice, and name all three slices in normalized_result.architectures.items. The fixture (arm64e.v1) and /bin/ls (arm64e) carry different arm64e variants, so both must pass. As a regression control, the universal x86_64 + arm64 fixture must still exit 0 with its arm64 UUID, as it does today.

Analysis or suspected cause

Source breadcrumbs, offered only to help retrace the evidence: at both tested builds, machArchitecture in src/domain/binaryTarget.ts maps CPU type 0x0100000c to arm64 for every subtype, including arm64e, and the FAT parser selects arm64 on an arm64 host. selectedArchitectureArguments in src/native/NativeMachoInspection.ts (added in #601) then passes -arch arm64 or --arch=arm64 to the slice-aware tools whenever a target lists two or more architectures. For a thin arm64e file or an arm64e-only universal file such as Calculator.app, the deduplicated availableArchitectures holds one entry, which matches the passing controls. This is a reading of the source, not a confirmed diagnosis.

Additional context

Related: #656 reports the Ghidra provider analyzing a different slice of the same kind of universal Mach-O. It is a separate provider and failure; this issue covers only inspect-macho on the native-macos provider.

Originally created by @coygeek on GitHub (Oct 6, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/674 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior On an Apple Silicon Mac, `rea inspect-macho` (MCP `inspect_macho`, provider `native-macos`) fails with a generic `execution_failure` on every universal Mach-O with x86_64 and arm64e slices that we tried: `/bin/ls`, `/bin/cat`, `/usr/bin/true`, `/usr/bin/zip`, and a clang-built x86_64 + arm64e test program. REA admits the target and selects its `arm64` architecture, but the operation returns no evidence. The same command succeeds on a universal x86_64 + arm64 binary, on an arm64e-only binary, and on the arm64e slice thinned out of the failing file. The error tells the caller to retry and run `rea doctor`, which cannot help because the failure is deterministic. Observed locally with both tested builds: ```text hello-x86_64-arm64e exit=1 hello-x86_64-arm64 exit=0 hello-arm64e exit=0 /bin/ls exit=1 ``` `lipo -info` reports `x86_64 arm64e` for the failing fixture and `x86_64 arm64` for the control. On this macOS build, `lipo -info /bin/ls` reports `x86_64 arm64e arm64e.x1`, and `/System/Applications/Calculator.app` (slices `arm64e arm64e.x1`, no x86_64) succeeds with exit 0. The JSON written to stdout for each failing target: ```json { "error": "Analysis failed", "code": "execution_failure", "category": "execution_failure", "message": "Analysis could not complete. Retry once; if it continues, run `rea doctor`.", "retryable": false, "remediation": { "action": "Analysis could not complete. Retry once; if it continues, run `rea doctor`." }, "details": { "provider_id": "native-macos", "operation": "inspect_macho" } } ``` With `REA_LOG_LEVEL=trace`, stderr adds only `{"level":50,...,"command":"inspect-macho","status":"error","msg":"CLI command failed"}`, so the output does not identify which native command failed. `rea list-architectures` on the same failing fixture exits 0 and reports both slices. For the x86_64 + arm64 control, the provenance in the successful result shows the provider passing the selected architecture to the slice-aware tools: `otool -h -l -arch arm64`, `nm -gjU -arch arm64`, `dyld_info -imports -arch arm64`, `dyld_info -exports -arch arm64`, `dwarfdump --uuid --arch=arm64`, `vtool -show-build -arch arm64`. When the native tools are run directly against the failing fixture or `/bin/ls`, `-arch arm64` fails for `otool`, `nm`, `dyld_info`, and `vtool`: ```text otool -h -l -arch arm64 -> exit 1 "file: ... does not contain architecture: arm64" nm -gjU -arch arm64 -> exit 1 "error: file: ... does not contain architecture: arm64" dyld_info -imports -arch arm64 -> exit 1 "... does not contain specified arch(s)" vtool -show-build -arch arm64 -> exit 1 "error: ... file does not contain architecture: arm64" dwarfdump --uuid --arch=arm64 -> exit 0, but prints no UUID ``` The two failing inputs carry different arm64e ABI variants. `lipo -detailed_info` reports the `/bin/ls` slice as `arm64e` (`PTR_AUTH_VERSION USERSPACE 0`) and the clang-built slice as `arm64e.v1` (`PTR_AUTH_VERSION USERSPACE 1`). On `/bin/ls`, `otool`, `nm`, and `dyld_info` all exit 0 with `-arch arm64e`. On the clang-built fixture, `otool` and `nm` exit 0 with `-arch arm64e`, but `dyld_info -imports/-exports -arch arm64e` exits 1 ("does not contain specified arch(s)") and exits 0 with `-arch arm64e.v1`, where `nm -arch arm64e.v1` exits 1. All three tools exit 0 with `-arch x86_64` on both inputs. ### Reproduction The steps use a two-line C program so the failing input is reproducible without relying on a particular macOS build. Run them on an Apple Silicon Mac with Xcode installed. ```bash WORK="$(mktemp -d)"; cd "$WORK" printf 'int main(void){return 0;}\n' > hello.c xcrun clang -arch x86_64 -arch arm64e -o hello-x86_64-arm64e hello.c # failing input xcrun clang -arch x86_64 -arch arm64 -o hello-x86_64-arm64 hello.c # control lipo hello-x86_64-arm64e -thin arm64e -output hello-arm64e # control lipo -info hello-x86_64-arm64e hello-x86_64-arm64 for t in hello-x86_64-arm64e hello-x86_64-arm64 hello-arm64e /bin/ls; do HOME="$WORK" npx -y rea-agents@4.0.1 inspect-macho "$t" --json > out.json 2>/dev/null echo "$t exit=$?" done ``` Optionally, compare the native tools that the provider runs with an explicit architecture: ```bash xcrun otool -h -l -arch arm64 hello-x86_64-arm64e > /dev/null; echo "otool -arch arm64 exit=$?" xcrun otool -h -l -arch arm64e hello-x86_64-arm64e > /dev/null; echo "otool -arch arm64e exit=$?" ``` ### Expected behavior `inspect_macho` is documented as "Inspect Mach-O slices, load commands, imports, exports, dependencies, build metadata, segments, sections, permissions, and exact command provenance without launching Hopper" (`src/contracts/nativeToolContracts.ts`). REA admits the universal x86_64 + arm64e file as a supported `mach-o` target with host-compatible architecture `arm64`, the same way it admits a universal x86_64 + arm64 file, which it inspects successfully. Since #601, the universal x86_64 + arm64 case returns evidence for the selected arm64 slice (its `normalized_result.uuid` matches that slice) while `architectures` lists both slices. The x86_64 + arm64e file should give the same kind of result: exit 0, evidence for the slice REA selected for the arm64 host (the arm64e slice), and both slices listed in `architectures`. ### Impact and scope - Impact level: P2 - Assessment status: supported - Impact basis: On Apple Silicon, `inspect_macho` fails on every universal x86_64 + arm64e Mach-O we tried. This class includes `/bin/ls` and other macOS system binaries that ship x86_64 and arm64e slices. The failure is deterministic. On the same inputs `list-architectures` keeps working, as does `inspect-signature` on `/bin/ls`. arm64e-only targets such as Calculator.app are unaffected, and no data is lost. - Workaround status: available - Workaround basis: `lipo <file> -thin arm64e -output <copy>` produces a thin copy that `inspect-macho` inspects successfully (verified locally). The resulting evidence names the copy's digest rather than the original file's, and the inventory loses the x86_64 slice, so the workaround preserves the slice data but not the original artifact identity. ### REA version REA: `rea-agents` 4.0.1 from npm (`latest` dist-tag; release tag `rea-agents-4.0.1`, commit `feeb19b8b3172b2827b80fde8f09570ce5aeed88`), and a source build of `main` at `19f8cddfd6f283188567b8ff1e49b810d79952ee`. Both builds behave identically. ### Runtime and platform Node.js v24.21.0; macOS 27.0.1 (26A434) on Apple Silicon (arm64); Xcode 27.0 (27A266a), Apple clang 21.0.0. ### Target and artifact context Universal (FAT) Mach-O executables with x86_64 and arm64e slices: macOS system binaries such as `/bin/ls` (`x86_64 arm64e arm64e.x1`) and a clang-built `x86_64 arm64e.v1` test program. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: `inspect_macho` tool contract in `src/contracts/nativeToolContracts.ts`, target admission in `src/domain/binaryTarget.ts`, which accepts the FAT file with host architecture `arm64`, and the successful x86_64 + arm64 control described under #601 and observed here. - Failure source: local CLI runs of `inspect-macho` on a clang-built universal x86_64 + arm64e fixture and on `/bin/ls`, with both tested builds, shown under Observed behavior. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ### Typed outcome or recovery expected With the universal x86_64 + arm64e fixture built in Reproduction, `rea inspect-macho hello-x86_64-arm64e --json` currently exits 1 with `execution_failure`. It passes when the command exits 0, `normalized_result.uuid` equals the arm64e UUID that `xcrun dwarfdump --uuid hello-x86_64-arm64e` prints, and `normalized_result.architectures.items` names both slices. A UUID equal to the x86_64 slice's fails the check, because an x86_64 result would hide the defect. `/bin/ls` on Apple Silicon must also exit 0, report the UUID that `xcrun dwarfdump --uuid /bin/ls` labels `(arm64e)` (not the `arm64e.x1` or x86_64 UUID) on a host that runs the plain arm64e slice, and name all three slices in `normalized_result.architectures.items`. The fixture (`arm64e.v1`) and `/bin/ls` (`arm64e`) carry different arm64e variants, so both must pass. As a regression control, the universal x86_64 + arm64 fixture must still exit 0 with its arm64 UUID, as it does today. ### Analysis or suspected cause Source breadcrumbs, offered only to help retrace the evidence: at both tested builds, `machArchitecture` in `src/domain/binaryTarget.ts` maps CPU type `0x0100000c` to `arm64` for every subtype, including arm64e, and the FAT parser selects `arm64` on an arm64 host. `selectedArchitectureArguments` in `src/native/NativeMachoInspection.ts` (added in #601) then passes `-arch arm64` or `--arch=arm64` to the slice-aware tools whenever a target lists two or more architectures. For a thin arm64e file or an arm64e-only universal file such as Calculator.app, the deduplicated `availableArchitectures` holds one entry, which matches the passing controls. This is a reading of the source, not a confirmed diagnosis. ### Additional context Related: #656 reports the Ghidra provider analyzing a different slice of the same kind of universal Mach-O. It is a separate provider and failure; this issue covers only `inspect-macho` on the `native-macos` provider.
bsakaguchi 2026-10-15 18:23:48 +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#314
No description provided.