[GH-ISSUE #4293] [Bug] Universal Mach-O signatures report one slice as the whole file #1698

Closed
opened 2026-10-16 21:09:12 +00:00 by bsakaguchi · 1 comment
Owner

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

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

When every slice of a universal Mach-O is signed, inspect_signature returns the identifier, CDHash, hardened-runtime state and entitlements of only the slice that codesign displays by default. It does not say which slice that is, does not add a limitation that other slices were not examined, and does not report that they differ. If the x86_64 slice has a different identifier, a different CDHash, no hardened runtime and get-task-allow instead of disable-library-validation, all of those facts are missing from the result. In the same result, designated_requirement lists the CDHashes of both slices while cdhashes lists only one.

Observed on the release build (exit status 0). Ground truth from codesign -d -a <arch>:

Slice Identifier CodeDirectory flags CDHash Entitlements
x86_64 com.example.x86slice 0x2(adhoc) 269615ed… get-task-allow
arm64 com.example.armslice 0x10002(adhoc,runtime) 133d5893… disable-library-validation

REA result (abridged):

{"signed":true,"identifier":"com.example.armslice","format":"Mach-O universal (x86_64 arm64)",
 "cdhashes":["133d589374281103e27b32ee243781884bc41413"],
 "designated_requirement":"cdhash H\"133d589374281103e27b32ee243781884bc41413\" or cdhash H\"269615ed3412eb7a8c0945e51c76355412db4b12\"",
 "entitlements":{"com.apple.security.cs.disable-library-validation":true},
 "hardened_runtime":true,
 "limitations":["Entitlements and designated requirements require separate bounded commands.","Inspected a digest-verified Mach-O byte snapshot; filesystem-bound signing context and trust validation are not examined."]}

The recorded provenance shows three aggregate commands: codesign -d --verbose=4, codesign -d -r- and codesign -d --entitlements :-. None selects an architecture. The Evidence subject.architecture is arm64, but the signature result itself does not name a slice, and the x86_64 slice's identifier, CDHash, missing hardened runtime and get-task-allow entitlement appear nowhere in it. Hashes are exact in the JSON above and abbreviated in the table. The hashes were reproducible across rebuilds with this toolchain and may differ with other toolchains. Repeated runs gave the same result.

Reproduction

Use the release build stated under Runtime and platform. Build a two-slice binary whose slices are ad hoc signed with different identities:

mkdir -p /tmp/rea-bug02 && cd /tmp/rea-bug02
printf 'int main(void){return 0;}\n' > main.c
printf '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>\n' > arm.plist
printf '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>com.apple.security.get-task-allow</key><true/></dict></plist>\n' > x86.plist
clang -arch arm64 -o slice-arm64 main.c
clang -arch x86_64 -o slice-x86_64 main.c
codesign -f -s - -i com.example.armslice -o runtime --entitlements arm.plist slice-arm64
codesign -f -s - -i com.example.x86slice --entitlements x86.plist slice-x86_64
lipo -create slice-x86_64 slice-arm64 -o fixture
for a in x86_64 arm64; do codesign -d -a "$a" -vvv --entitlements - --xml fixture; done
codesign -d -r- fixture
npx -y rea-agents@6.5.0 inspect-signature ./fixture --json

Expected behavior

REA's tool contract describes inspect_signature as inspecting "the active Mach-O executable's code-signing identity, hashes, authorities, requirements, entitlements, hardened-runtime state". Apple's TN3126 states: "In a universal binary, each architecture is signed independently, each with its own code directory." The codesign(1) manual says "The default for display is to report on the native architecture of the host system", so plain codesign -d describes one slice. REA already applies this per-slice view when a slice is unsigned: it runs codesign -d -a <arch> for each slice and adds Unsigned Mach-O slices: …. For a universal file whose slices are all signed, the result should either report each slice's identity, CDHash, runtime flag and entitlements, or name the slice its fields describe and say in a limitation that other slices were not reported (or that they differ). cdhashes and designated_requirement should not contradict each other without explanation.

Reading the result as a description of the selected slice does not resolve the problem. The Evidence subject.architecture for this result is arm64, and the "Static inspection" section of REA's native investigation guide, which describes universal inputs for inspect_macho, says "The normalized result describes the selected Mach-O image." Applying that convention to inspect_signature is an inference, but it is the only selection rule the guide states. None of the three codesign commands passes -a <subject.architecture>. The identity fields therefore match the subject only when the selected slice happens to be the host-native slice that codesign displays by default, and designated_requirement describes the whole file rather than the selected image.

Impact and scope

An analyst who uses inspect_signature to decide what a universal binary is allowed to do sees only the host-native slice's entitlements and hardened-runtime state. In the fixture, the x86_64 slice, which runs under Rosetta on Apple silicon and natively on Intel Macs, carries get-task-allow and no hardened runtime, yet the result shows hardened runtime on and no such entitlement. Per the codesign(1) default, the same file would describe the x86_64 slice on an Intel host. That consequence comes from the manual and was not run here. The same guide connects these fields to debugging: a hardened-runtime target without get-task-allow "cannot be debugged; inspect_signature reports this as the debugger-attach facet" (L482-L485). The main build rewords that passage: inspect_signature "reports the signature's hardened_runtime value and its entitlements, including com.apple.security.get-task-allow when it is present", which are exactly the two facts this result misreports for the x86_64 slice. For that slice, the debugging conclusion would be wrong.

Workaround: Running codesign -d -a <arch> -vvv --entitlements - --xml for each slice listed by lipo -archs gives the complete per-slice facts, though outside REA Evidence.

REA version

6.5.0 (tag rea-agents-6.5.0; signature capture unchanged on main)

Runtime and platform

  • REA: rea-agents 6.5.0 from npm (--version prints 6.5.0), release tag rea-agents-6.5.0 at commit 3b79af28e80785afce9f5998d8f03289402a1847. Main at d52a488 leaves the signature capture code unchanged. Its NativeMacOSProvider.ts changes touch only plist path resolution.
  • Host: macOS 27.0.1 (26A434) arm64, Xcode 27.0 (27A266a) clang, codesign and lipo, Node.js v26.10.0.

Target and artifact context

self-built universal (x86_64 + arm64) Mach-O executable; each slice ad hoc signed with a different identifier, runtime flag and entitlements

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: REA's tool description for inspect_signature (nativeToolContracts.ts L106-L110), Apple's TN3126: Inside Code Signing: Hashes ("each architecture is signed independently, each with its own code directory"), and the codesign(1) manual's --architecture entry on the host OS.
  • Failure source: rea inspect-signature ./fixture --json on the release build compared with codesign -d -a x86_64 -vvv --entitlements - --xml fixture. The x86_64 slice's identity, CDHash, runtime flag and entitlements are absent, and no limitation mentions slices.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

This investigator built the fixture and ran the commands above on the release build. The provider runs only the aggregate codesign -d, -d -r- and -d --entitlements :- commands (NativeMacOSProvider.ts L590-L615). It runs per-slice codesign -d -a <arch> only when an aggregate query reports an unsigned slice (L639-L656).

Typed outcome or recovery expected

Using the fixture above, the failing result reports com.example.armslice, one CDHash, hardened_runtime: true and only disable-library-validation, with no slice named and no slice limitation. A restored result either reports both slices' identifiers, CDHashes, runtime state and entitlements, or names arm64 as the slice described and states in a limitation that the x86_64 slice was not reported or differs. The CDHash fields must not contradict designated_requirement without explanation. Discriminating controls: a thin signed binary (for example slice-arm64 alone) keeps its current single-slice result with no new slice limitation, and a universal file with one unsigned slice keeps its existing Unsigned Mach-O slices: … limitation.

Analysis or suspected cause

No response

Additional context

  • Open PR #3583 ("check every slice of a signed universal Mach-O") queries every slice of a signed universal file, but only to report unsigned slices. Its own test "adds no slice limitation when every universal slice is signed" asserts that a fully signed universal file gets no slice limitation, so it does not cover slices that are signed differently.
  • Closed #684 and merged PR #701 introduced the mixed signed/unsigned slice handling. Merged PR #601 established that slice-specific observations from inspect_macho belong to the selected architecture, but that change did not touch inspect_signature.
  • designated_requirement lists both CDHashes because codesign -d -r- without -a prints the implicit ad hoc requirement that covers every slice. This report does not decide whether that field should describe the whole file. It asks only that it not silently disagree with cdhashes.
Originally created by @coygeek on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4293 ### 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 When every slice of a universal Mach-O is signed, `inspect_signature` returns the identifier, CDHash, hardened-runtime state and entitlements of only the slice that `codesign` displays by default. It does not say which slice that is, does not add a limitation that other slices were not examined, and does not report that they differ. If the x86_64 slice has a different identifier, a different CDHash, no hardened runtime and `get-task-allow` instead of `disable-library-validation`, all of those facts are missing from the result. In the same result, `designated_requirement` lists the CDHashes of both slices while `cdhashes` lists only one. Observed on the release build (exit status 0). Ground truth from `codesign -d -a <arch>`: | Slice | Identifier | CodeDirectory flags | CDHash | Entitlements | |---|---|---|---|---| | x86_64 | `com.example.x86slice` | `0x2(adhoc)` | `269615ed…` | `get-task-allow` | | arm64 | `com.example.armslice` | `0x10002(adhoc,runtime)` | `133d5893…` | `disable-library-validation` | REA result (abridged): ```json {"signed":true,"identifier":"com.example.armslice","format":"Mach-O universal (x86_64 arm64)", "cdhashes":["133d589374281103e27b32ee243781884bc41413"], "designated_requirement":"cdhash H\"133d589374281103e27b32ee243781884bc41413\" or cdhash H\"269615ed3412eb7a8c0945e51c76355412db4b12\"", "entitlements":{"com.apple.security.cs.disable-library-validation":true}, "hardened_runtime":true, "limitations":["Entitlements and designated requirements require separate bounded commands.","Inspected a digest-verified Mach-O byte snapshot; filesystem-bound signing context and trust validation are not examined."]} ``` The recorded provenance shows three aggregate commands: `codesign -d --verbose=4`, `codesign -d -r-` and `codesign -d --entitlements :-`. None selects an architecture. The Evidence `subject.architecture` is `arm64`, but the signature result itself does not name a slice, and the x86_64 slice's identifier, CDHash, missing hardened runtime and `get-task-allow` entitlement appear nowhere in it. Hashes are exact in the JSON above and abbreviated in the table. The hashes were reproducible across rebuilds with this toolchain and may differ with other toolchains. Repeated runs gave the same result. ### Reproduction Use the release build stated under Runtime and platform. Build a two-slice binary whose slices are ad hoc signed with different identities: ```sh mkdir -p /tmp/rea-bug02 && cd /tmp/rea-bug02 printf 'int main(void){return 0;}\n' > main.c printf '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>\n' > arm.plist printf '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>com.apple.security.get-task-allow</key><true/></dict></plist>\n' > x86.plist clang -arch arm64 -o slice-arm64 main.c clang -arch x86_64 -o slice-x86_64 main.c codesign -f -s - -i com.example.armslice -o runtime --entitlements arm.plist slice-arm64 codesign -f -s - -i com.example.x86slice --entitlements x86.plist slice-x86_64 lipo -create slice-x86_64 slice-arm64 -o fixture for a in x86_64 arm64; do codesign -d -a "$a" -vvv --entitlements - --xml fixture; done codesign -d -r- fixture npx -y rea-agents@6.5.0 inspect-signature ./fixture --json ``` ### Expected behavior REA's tool contract describes `inspect_signature` as inspecting "the active Mach-O executable's code-signing identity, hashes, authorities, requirements, entitlements, hardened-runtime state". Apple's TN3126 states: "In a universal binary, each architecture is signed independently, each with its own code directory." The `codesign(1)` manual says "The default for display is to report on the native architecture of the host system", so plain `codesign -d` describes one slice. REA already applies this per-slice view when a slice is unsigned: it runs `codesign -d -a <arch>` for each slice and adds `Unsigned Mach-O slices: …`. For a universal file whose slices are all signed, the result should either report each slice's identity, CDHash, runtime flag and entitlements, or name the slice its fields describe and say in a limitation that other slices were not reported (or that they differ). `cdhashes` and `designated_requirement` should not contradict each other without explanation. Reading the result as a description of the selected slice does not resolve the problem. The Evidence `subject.architecture` for this result is `arm64`, and the "Static inspection" section of REA's [native investigation guide](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/docs/native-investigation.md#L264-L269), which describes universal inputs for `inspect_macho`, says "The normalized result describes the selected Mach-O image." Applying that convention to `inspect_signature` is an inference, but it is the only selection rule the guide states. None of the three `codesign` commands passes `-a <subject.architecture>`. The identity fields therefore match the subject only when the selected slice happens to be the host-native slice that `codesign` displays by default, and `designated_requirement` describes the whole file rather than the selected image. ### Impact and scope An analyst who uses `inspect_signature` to decide what a universal binary is allowed to do sees only the host-native slice's entitlements and hardened-runtime state. In the fixture, the x86_64 slice, which runs under Rosetta on Apple silicon and natively on Intel Macs, carries `get-task-allow` and no hardened runtime, yet the result shows hardened runtime on and no such entitlement. Per the `codesign(1)` default, the same file would describe the x86_64 slice on an Intel host. That consequence comes from the manual and was not run here. The same guide connects these fields to debugging: a hardened-runtime target without `get-task-allow` "cannot be debugged; `inspect_signature` reports this as the debugger-attach facet" ([L482-L485](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/docs/native-investigation.md#L482-L485)). The `main` build rewords that passage: `inspect_signature` "reports the signature's `hardened_runtime` value and its `entitlements`, including `com.apple.security.get-task-allow` when it is present", which are exactly the two facts this result misreports for the x86_64 slice. For that slice, the debugging conclusion would be wrong. Workaround: Running `codesign -d -a <arch> -vvv --entitlements - --xml` for each slice listed by `lipo -archs` gives the complete per-slice facts, though outside REA Evidence. ### REA version 6.5.0 (tag `rea-agents-6.5.0`; signature capture unchanged on main) ### Runtime and platform - REA: `rea-agents` 6.5.0 from npm (`--version` prints `6.5.0`), release tag `rea-agents-6.5.0` at commit `3b79af28e80785afce9f5998d8f03289402a1847`. Main at `d52a488` leaves the signature capture code unchanged. Its `NativeMacOSProvider.ts` changes touch only plist path resolution. - Host: macOS 27.0.1 (26A434) arm64, Xcode 27.0 (27A266a) `clang`, `codesign` and `lipo`, Node.js v26.10.0. ### Target and artifact context self-built universal (x86_64 + arm64) Mach-O executable; each slice ad hoc signed with a different identifier, runtime flag and entitlements ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: REA's tool description for `inspect_signature` ([nativeToolContracts.ts L106-L110](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/src/contracts/native/nativeToolContracts.ts#L106-L110)), Apple's [TN3126: Inside Code Signing: Hashes](https://developer.apple.com/documentation/technotes/tn3126-inside-code-signing-hashes) ("each architecture is signed independently, each with its own code directory"), and the `codesign(1)` manual's `--architecture` entry on the host OS. - Failure source: `rea inspect-signature ./fixture --json` on the release build compared with `codesign -d -a x86_64 -vvv --entitlements - --xml fixture`. The x86_64 slice's identity, CDHash, runtime flag and entitlements are absent, and no limitation mentions slices. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete This investigator built the fixture and ran the commands above on the release build. The provider runs only the aggregate `codesign -d`, `-d -r-` and `-d --entitlements :-` commands ([NativeMacOSProvider.ts L590-L615](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/src/native/NativeMacOSProvider.ts#L590-L615)). It runs per-slice `codesign -d -a <arch>` only when an aggregate query reports an unsigned slice ([L639-L656](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/src/native/NativeMacOSProvider.ts#L639-L656)). ### Typed outcome or recovery expected Using the fixture above, the failing result reports `com.example.armslice`, one CDHash, `hardened_runtime: true` and only `disable-library-validation`, with no slice named and no slice limitation. A restored result either reports both slices' identifiers, CDHashes, runtime state and entitlements, or names `arm64` as the slice described and states in a limitation that the x86_64 slice was not reported or differs. The CDHash fields must not contradict `designated_requirement` without explanation. Discriminating controls: a thin signed binary (for example `slice-arm64` alone) keeps its current single-slice result with no new slice limitation, and a universal file with one unsigned slice keeps its existing `Unsigned Mach-O slices: …` limitation. ### Analysis or suspected cause _No response_ ### Additional context - Open PR #3583 ("check every slice of a signed universal Mach-O") queries every slice of a signed universal file, but only to report unsigned slices. Its own test "adds no slice limitation when every universal slice is signed" asserts that a fully signed universal file gets no slice limitation, so it does not cover slices that are signed differently. - Closed #684 and merged PR #701 introduced the mixed signed/unsigned slice handling. Merged PR #601 established that slice-specific observations from `inspect_macho` belong to the selected architecture, but that change did not touch `inspect_signature`. - `designated_requirement` lists both CDHashes because `codesign -d -r-` without `-a` prints the implicit ad hoc requirement that covers every slice. This report does not decide whether that field should describe the whole file. It asks only that it not silently disagree with `cdhashes`.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

All three signature facets explicitly select the active architecture via -a. Universal results now include a partial-signature-coverage limitation naming that architecture and saying the other slices metadata is not returned; signing-state enumeration remains separate. The all-signed universal regression varies selected arm64/x86_64, requires matching identifier/requirement/entitlements and checks all facet commands select the same slice.

Implementation: NativeMacOSProvider.ts, NativeMacOSProvider.ts.

The relevant regression checks in nativeInventory.golden.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380733065 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. All three signature facets explicitly select the active architecture via -a. Universal results now include a partial-signature-coverage limitation naming that architecture and saying the other slices metadata is not returned; signing-state enumeration remains separate. The all-signed universal regression varies selected arm64/x86_64, requires matching identifier/requirement/entitlements and checks all facet commands select the same slice. Implementation: [NativeMacOSProvider.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/native/NativeMacOSProvider.ts#L753), [NativeMacOSProvider.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/native/NativeMacOSProvider.ts#L820). The relevant regression checks in [nativeInventory.golden.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/native/nativeInventory.golden.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#1698
No description provided.