[GH-ISSUE #684] [Bug] inspect-signature fails with execution_failure on universal Mach-O binaries with a signed arm64/arm64e slice and an unsigned x86_64 slice #313

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/684

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

rea inspect-signature (MCP inspect_signature, provider native-macos) exits 1 with the generic execution_failure ("Analysis could not complete. Retry once; if it continues, run rea doctor.") on a universal Mach-O whose arm64 or arm64e slice is signed (here ad hoc linker-signed) while its x86_64 slice is unsigned. A default clang -arch x86_64 -arch arm64 build on Apple Silicon produces this state. The same command returns a typed result for a thin signed file, a thin unsigned file, a universal file signed in every slice, a universal file with no signature in any slice, and /bin/ls. The failure is deterministic, so retrying does not help (the error itself says retryable: false while its message says to retry), and the caller gets no signature facts for either slice.

Observed locally, identical for both tested builds:

uni exit=1
uni-e exit=1
thin exit=0
uni-adhoc exit=0
uni-nosig exit=0
x86only exit=0
/bin/ls exit=0

lipo -info reports x86_64 arm64 for uni and x86_64 arm64e for uni-e. For both failing files, stdout is:

{
  "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_signature"
  }
}

With REA_LOG_LEVEL=trace, stderr adds only {"level":50,...,"command":"inspect-signature","status":"error","msg":"CLI command failed"}, which does not identify the failing native command.

The controls return typed results: thin gives signed: true, format: "Mach-O thin (arm64)" and a cdhash designated requirement; uni-adhoc gives signed: true, format: "Mach-O universal (x86_64 arm64)" and a designated requirement listing two cdhashes; uni-nosig and x86only give signed: false with the limitation "Artifact is not signed.". On uni, rea list-architectures and rea inspect-macho both exit 0. On uni-e, rea list-architectures exits 0, while rea inspect-macho fails separately, as reported in #674 for x86_64 + arm64e files; the inspect-signature failure here also occurs on uni, which #674 does not affect.

Running the provider's three codesign commands directly on uni (uni-e gives the same exit codes):

codesign -d --verbose=4 ./uni          -> exit 0; Format=Mach-O universal (x86_64 arm64), CodeDirectory ... flags=0x20002(adhoc,linker-signed)
codesign -d -r- ./uni                  -> exit 1; "./uni: code object is not signed at all"
codesign -d --entitlements :- ./uni    -> exit 0

codesign -d -a x86_64 ./uni reports "code object is not signed at all"; codesign -d -a arm64 ./uni reports a signed slice. Per slice, codesign -d -r- -a arm64 ./uni exits 0 and codesign -d -r- -a x86_64 ./uni exits 1 (for uni-e, -a arm64e exits 0 and -a x86_64 exits 1). For the controls thin and uni-adhoc, codesign -d -r- exits 0.

Reproduction

Run on an Apple Silicon Mac with Xcode or the Command Line Tools installed; both tested builds behave the same.

WORK="$(mktemp -d)"; cd "$WORK"
printf 'int main(void){return 0;}\n' > t.c
xcrun clang -arch x86_64 -arch arm64  -o uni   t.c   # failing input
xcrun clang -arch x86_64 -arch arm64e -o uni-e t.c   # failing input
xcrun clang -arch arm64 -o thin t.c                  # control: thin, linker-signed
cp uni uni-adhoc && codesign -s - -f uni-adhoc       # control: universal, every slice ad hoc signed
cp uni uni-nosig && codesign --remove-signature uni-nosig   # control: universal, no slice signed
lipo uni -thin x86_64 -output x86only                # control: thin, unsigned
for t in uni uni-e thin uni-adhoc uni-nosig x86only /bin/ls; do
  HOME="$WORK" npx -y rea-agents@4.0.1 inspect-signature "$t" --json > "out-$(basename "$t").json" 2>/dev/null
  echo "$t exit=$?"
done

To see the per-slice signing state and the commands the provider runs:

codesign -d -a x86_64 ./uni                                  # "code object is not signed at all"
codesign -d -a arm64 --verbose=4 ./uni 2>&1 | grep CodeDirectory   # flags=0x20002(adhoc,linker-signed)
codesign -d --verbose=4 ./uni; echo "exit=$?"
codesign -d -r- ./uni; echo "exit=$?"
codesign -d --entitlements :- ./uni; echo "exit=$?"

Expected behavior

inspect_signature is documented as "Inspect the active artifact's code-signing identity, hashes, authorities, requirements, entitlements, hardened-runtime state, and exact command provenance" (src/contracts/nativeToolContracts.ts), and REA admits these files as Mach-O targets. The provider already treats codesign's "code object is not signed at all" output as an observation rather than a failure: an unsigned artifact yields a typed result with signed: false and the limitation "Artifact is not signed." (src/native/parsers/codesign.ts), and the regression test "distinguishes a codesign execution failure from an observed unsigned artifact" (tests/boundary/providers/native/nativeInventory.golden.test.ts) expects ok: true for a fully unsigned artifact (every codesign call returning that message) and an error for a missing file. PR #503, which added that test, summarizes the rule as: "Reject failed codesign inspections instead of returning signed: true. Preserve successful unsigned observations and propagate failures from signed-artifact requirement/entitlement queries." The current code follows the last clause literally: the display call shows a signature, so the later codesign -d -r- exit 1 is propagated. That exit is not a failed query, though: codesign ran and reported the same "code object is not signed at all" observation, this time about the unsigned x86_64 slice, and the test covers only a fully unsigned file, not this mixed state. Under the observation-versus-failure distinction above, and the tool design guide's rule to "Make real pagination, truncation, partial failure, cancellation, and unavailable states explicit" with relevant limitations (docs/tool-design.md, "Define the contract"), the caller should get exit 0 and a typed inspect_signature result for these files, as for the controls. Proposed acceptance criterion beyond the contract violation: that result should state that the x86_64 slice is unsigned rather than report the file as signed without qualification. The project's contract does not say how a universal file with differently signed slices should be represented, so the exact result shape is the maintainers' choice.

Impact and scope

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The observed reproduction shows inspect_signature failing deterministically for a class of valid universal Mach-O inputs (signed arm64 or arm64e slice, unsigned x86_64 slice) and returning a misleading retry instruction instead of signature facts; signed universal files, fully unsigned universal files, thin files, and /bin/ls are unaffected, and no data is lost or corrupted.
  • Workaround status: available
  • Workaround basis: Thinning copies of the file with lipo <file> -thin arm64 -output <copy> and lipo <file> -thin x86_64 -output <copy> and inspecting each copy was verified on the main build: the arm64 copy returns signed: true with a designated requirement whose CDHash equals the arm64 slice's CDHash in the original file, and the x86_64 copy returns signed: false; the original file is not modified, though the caller must run two inspections on derived artifacts.

Locally built universal tools are the main case: with default flags, as in the reproduction, clang on Apple Silicon ad hoc linker-signs the arm64 or arm64e slice and leaves the x86_64 slice unsigned. Distributed apps are usually signed in every slice. On the test Mac, running codesign -d and codesign -d -r- over the universal executables in /Applications/*.app/Contents/MacOS/, /opt/homebrew/bin/ and /usr/local/bin/ found this mixed state (display succeeds, -r- fails) in 1 of 692. The MCP tool inspect_signature uses the same provider; only the CLI was exercised.

REA version

rea-agents 4.0.1 from npm (release tag rea-agents-4.0.1, commit feeb19b8b3172b2827b80fde8f09570ce5aeed88) and a source build of main at 7f1ae42f71ce85547d317eed5e62fa330d9876bb, both run through the CLI with an empty temporary HOME. Both builds behave identically.

Runtime and platform

Node.js 24.21.0; macOS 27.0.1 (26A434) on Apple Silicon (arm64); Apple clang 21.0.0 (clang-2100.3.34.2). Not tested: Intel Macs, older macOS releases.

Target and artifact context

Universal (FAT) Mach-O executables built with clang from a two-line C program: x86_64 arm64 and x86_64 arm64e, with the arm64/arm64e slice ad hoc linker-signed and the x86_64 slice unsigned.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: tool description in src/contracts/nativeToolContracts.ts (inspect_signature); unsigned result contract in src/native/parsers/codesign.ts (signed: !unsigned, limitation "Artifact is not signed."); regression test "distinguishes a codesign execution failure from an observed unsigned artifact" in tests/boundary/providers/native/nativeInventory.golden.test.ts; PR #503 summary quoted under Expected behavior; docs/tool-design.md, "Define the contract".
  • Failure source: CLI exit codes and JSON quoted under Observed behavior, observed on both tested builds; direct codesign exit codes for the same files.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Typed outcome or recovery expected

Failing now: rea inspect-signature uni --json and rea inspect-signature uni-e --json from the reproduction exit 1 with execution_failure. Restored: both exit 0 with a typed inspect_signature result. Proposed additional criterion, per Expected behavior: that result states that the x86_64 slice is unsigned. Controls that must keep their current behavior: thin and uni-adhoc return signed: true with their designated requirements; uni-nosig and x86only return signed: false with "Artifact is not signed."; and a real codesign execution failure (the missing-file case in the existing regression test) still returns an error rather than a result. Restoration must not come from accepting every nonzero codesign exit as success.

Analysis or suspected cause

Source-derived reasoning, not verified by instrumenting REA: in #inspectSignature (src/native/NativeMacOSProvider.ts, lines 324-390 on the main build), the unsigned flag is set only when the codesign -d --verbose=4 output matches "not signed at all". That command exits 0 with a signed display for these files, so unsigned is false, and the following codesign -d -r- call runs with acceptNonZero: unsigned. Its exit 1 is then treated as a command failure. No architecture is passed to codesign. This reading matches the observed exit codes but was not confirmed with a debugger, because the trace log contains only the generic CLI error.

Additional context

Related: #674 reports inspect-macho failing on universal x86_64 + arm64e files. It is a separate operation and failure; this issue covers only inspect-signature, which also fails on the x86_64 + arm64 file that #674 does not affect.

Originally created by @coygeek on GitHub (Oct 6, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/684 ### 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 `rea inspect-signature` (MCP `inspect_signature`, provider `native-macos`) exits 1 with the generic `execution_failure` ("Analysis could not complete. Retry once; if it continues, run `rea doctor`.") on a universal Mach-O whose arm64 or arm64e slice is signed (here ad hoc linker-signed) while its x86_64 slice is unsigned. A default `clang -arch x86_64 -arch arm64` build on Apple Silicon produces this state. The same command returns a typed result for a thin signed file, a thin unsigned file, a universal file signed in every slice, a universal file with no signature in any slice, and `/bin/ls`. The failure is deterministic, so retrying does not help (the error itself says `retryable: false` while its message says to retry), and the caller gets no signature facts for either slice. Observed locally, identical for both tested builds: ```text uni exit=1 uni-e exit=1 thin exit=0 uni-adhoc exit=0 uni-nosig exit=0 x86only exit=0 /bin/ls exit=0 ``` `lipo -info` reports `x86_64 arm64` for `uni` and `x86_64 arm64e` for `uni-e`. For both failing files, stdout is: ```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_signature" } } ``` With `REA_LOG_LEVEL=trace`, stderr adds only `{"level":50,...,"command":"inspect-signature","status":"error","msg":"CLI command failed"}`, which does not identify the failing native command. The controls return typed results: `thin` gives `signed: true`, `format: "Mach-O thin (arm64)"` and a `cdhash` designated requirement; `uni-adhoc` gives `signed: true`, `format: "Mach-O universal (x86_64 arm64)"` and a designated requirement listing two cdhashes; `uni-nosig` and `x86only` give `signed: false` with the limitation "Artifact is not signed.". On `uni`, `rea list-architectures` and `rea inspect-macho` both exit 0. On `uni-e`, `rea list-architectures` exits 0, while `rea inspect-macho` fails separately, as reported in #674 for x86_64 + arm64e files; the `inspect-signature` failure here also occurs on `uni`, which #674 does not affect. Running the provider's three codesign commands directly on `uni` (`uni-e` gives the same exit codes): ```text codesign -d --verbose=4 ./uni -> exit 0; Format=Mach-O universal (x86_64 arm64), CodeDirectory ... flags=0x20002(adhoc,linker-signed) codesign -d -r- ./uni -> exit 1; "./uni: code object is not signed at all" codesign -d --entitlements :- ./uni -> exit 0 ``` `codesign -d -a x86_64 ./uni` reports "code object is not signed at all"; `codesign -d -a arm64 ./uni` reports a signed slice. Per slice, `codesign -d -r- -a arm64 ./uni` exits 0 and `codesign -d -r- -a x86_64 ./uni` exits 1 (for `uni-e`, `-a arm64e` exits 0 and `-a x86_64` exits 1). For the controls `thin` and `uni-adhoc`, `codesign -d -r-` exits 0. ### Reproduction Run on an Apple Silicon Mac with Xcode or the Command Line Tools installed; both tested builds behave the same. ```bash WORK="$(mktemp -d)"; cd "$WORK" printf 'int main(void){return 0;}\n' > t.c xcrun clang -arch x86_64 -arch arm64 -o uni t.c # failing input xcrun clang -arch x86_64 -arch arm64e -o uni-e t.c # failing input xcrun clang -arch arm64 -o thin t.c # control: thin, linker-signed cp uni uni-adhoc && codesign -s - -f uni-adhoc # control: universal, every slice ad hoc signed cp uni uni-nosig && codesign --remove-signature uni-nosig # control: universal, no slice signed lipo uni -thin x86_64 -output x86only # control: thin, unsigned for t in uni uni-e thin uni-adhoc uni-nosig x86only /bin/ls; do HOME="$WORK" npx -y rea-agents@4.0.1 inspect-signature "$t" --json > "out-$(basename "$t").json" 2>/dev/null echo "$t exit=$?" done ``` To see the per-slice signing state and the commands the provider runs: ```bash codesign -d -a x86_64 ./uni # "code object is not signed at all" codesign -d -a arm64 --verbose=4 ./uni 2>&1 | grep CodeDirectory # flags=0x20002(adhoc,linker-signed) codesign -d --verbose=4 ./uni; echo "exit=$?" codesign -d -r- ./uni; echo "exit=$?" codesign -d --entitlements :- ./uni; echo "exit=$?" ``` ### Expected behavior `inspect_signature` is documented as "Inspect the active artifact's code-signing identity, hashes, authorities, requirements, entitlements, hardened-runtime state, and exact command provenance" (`src/contracts/nativeToolContracts.ts`), and REA admits these files as Mach-O targets. The provider already treats codesign's "code object is not signed at all" output as an observation rather than a failure: an unsigned artifact yields a typed result with `signed: false` and the limitation "Artifact is not signed." (`src/native/parsers/codesign.ts`), and the regression test "distinguishes a codesign execution failure from an observed unsigned artifact" (`tests/boundary/providers/native/nativeInventory.golden.test.ts`) expects `ok: true` for a fully unsigned artifact (every codesign call returning that message) and an error for a missing file. PR #503, which added that test, summarizes the rule as: "Reject failed `codesign` inspections instead of returning `signed: true`. Preserve successful unsigned observations and propagate failures from signed-artifact requirement/entitlement queries." The current code follows the last clause literally: the display call shows a signature, so the later `codesign -d -r-` exit 1 is propagated. That exit is not a failed query, though: codesign ran and reported the same "code object is not signed at all" observation, this time about the unsigned x86_64 slice, and the test covers only a fully unsigned file, not this mixed state. Under the observation-versus-failure distinction above, and the tool design guide's rule to "Make real pagination, truncation, partial failure, cancellation, and unavailable states explicit" with relevant limitations (`docs/tool-design.md`, "Define the contract"), the caller should get exit 0 and a typed `inspect_signature` result for these files, as for the controls. Proposed acceptance criterion beyond the contract violation: that result should state that the x86_64 slice is unsigned rather than report the file as signed without qualification. The project's contract does not say how a universal file with differently signed slices should be represented, so the exact result shape is the maintainers' choice. ### Impact and scope - Impact level: P2 - Assessment status: supported - Impact basis: The observed reproduction shows `inspect_signature` failing deterministically for a class of valid universal Mach-O inputs (signed arm64 or arm64e slice, unsigned x86_64 slice) and returning a misleading retry instruction instead of signature facts; signed universal files, fully unsigned universal files, thin files, and `/bin/ls` are unaffected, and no data is lost or corrupted. - Workaround status: available - Workaround basis: Thinning copies of the file with `lipo <file> -thin arm64 -output <copy>` and `lipo <file> -thin x86_64 -output <copy>` and inspecting each copy was verified on the `main` build: the arm64 copy returns `signed: true` with a designated requirement whose CDHash equals the arm64 slice's CDHash in the original file, and the x86_64 copy returns `signed: false`; the original file is not modified, though the caller must run two inspections on derived artifacts. Locally built universal tools are the main case: with default flags, as in the reproduction, clang on Apple Silicon ad hoc linker-signs the arm64 or arm64e slice and leaves the x86_64 slice unsigned. Distributed apps are usually signed in every slice. On the test Mac, running `codesign -d` and `codesign -d -r-` over the universal executables in `/Applications/*.app/Contents/MacOS/`, `/opt/homebrew/bin/` and `/usr/local/bin/` found this mixed state (display succeeds, `-r-` fails) in 1 of 692. The MCP tool `inspect_signature` uses the same provider; only the CLI was exercised. ### REA version `rea-agents` 4.0.1 from npm (release tag `rea-agents-4.0.1`, commit `feeb19b8b3172b2827b80fde8f09570ce5aeed88`) and a source build of `main` at `7f1ae42f71ce85547d317eed5e62fa330d9876bb`, both run through the CLI with an empty temporary `HOME`. Both builds behave identically. ### Runtime and platform Node.js 24.21.0; macOS 27.0.1 (26A434) on Apple Silicon (arm64); Apple clang 21.0.0 (clang-2100.3.34.2). Not tested: Intel Macs, older macOS releases. ### Target and artifact context Universal (FAT) Mach-O executables built with clang from a two-line C program: `x86_64 arm64` and `x86_64 arm64e`, with the arm64/arm64e slice ad hoc linker-signed and the x86_64 slice unsigned. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: tool description in `src/contracts/nativeToolContracts.ts` (`inspect_signature`); unsigned result contract in `src/native/parsers/codesign.ts` (`signed: !unsigned`, limitation "Artifact is not signed."); regression test "distinguishes a codesign execution failure from an observed unsigned artifact" in `tests/boundary/providers/native/nativeInventory.golden.test.ts`; PR #503 summary quoted under Expected behavior; `docs/tool-design.md`, "Define the contract". - Failure source: CLI exit codes and JSON quoted under Observed behavior, observed on both tested builds; direct `codesign` exit codes for the same files. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ### Typed outcome or recovery expected Failing now: `rea inspect-signature uni --json` and `rea inspect-signature uni-e --json` from the reproduction exit 1 with `execution_failure`. Restored: both exit 0 with a typed `inspect_signature` result. Proposed additional criterion, per Expected behavior: that result states that the x86_64 slice is unsigned. Controls that must keep their current behavior: `thin` and `uni-adhoc` return `signed: true` with their designated requirements; `uni-nosig` and `x86only` return `signed: false` with "Artifact is not signed."; and a real codesign execution failure (the missing-file case in the existing regression test) still returns an error rather than a result. Restoration must not come from accepting every nonzero codesign exit as success. ### Analysis or suspected cause Source-derived reasoning, not verified by instrumenting REA: in `#inspectSignature` (`src/native/NativeMacOSProvider.ts`, lines 324-390 on the `main` build), the `unsigned` flag is set only when the `codesign -d --verbose=4` output matches "not signed at all". That command exits 0 with a signed display for these files, so `unsigned` is false, and the following `codesign -d -r-` call runs with `acceptNonZero: unsigned`. Its exit 1 is then treated as a command failure. No architecture is passed to codesign. This reading matches the observed exit codes but was not confirmed with a debugger, because the trace log contains only the generic CLI error. ### Additional context Related: #674 reports `inspect-macho` failing on universal x86_64 + arm64e files. It is a separate operation and failure; this issue covers only `inspect-signature`, which also fails on the x86_64 + arm64 file that #674 does not affect.
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#313
No description provided.