Skip to content

fix(native): select the preferred universal slice for metadata readers - #9793

Open
chordpli wants to merge 5 commits into
morluto:mainfrom
chordpli:fix/macho-slice-preference
Open

chordpli wants to merge 5 commits into
morluto:mainfrom
chordpli:fix/macho-slice-preference

Conversation

@chordpli

@chordpli chordpli commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #9792.

Stock macOS tools such as /usr/bin/file ship three slices: x86_64, arm64 and arm64e. inspect_native_initializers had no input that reached either arm64 slice. --architecture arm64 was refused as "several arm64 slices", and arm64e isn't an accepted value. inspect_macho reads the same file fine because it picks one slice by subtype preference.

After this change, the metadata readers resolve arm64 by the subtype preference order inspect_macho also uses (arm64, then arm64e, then arm64v8). On the stock tools below, they now read the same slice inspect_macho reads. When other slices of that family go unread, limitations names them ("Only the selected arm64 slice ... was read"), so the result doesn't read as covering the whole family.

Problem and expected behavior

Smallest trigger: rea inspect-native-initializers /usr/bin/file --architecture arm64. The broken invariant: an architecture refusal should name an input that works, and one universal file should resolve to the same slice across REA tools.

selectNativeImage (src/native/metadata/MachoSliceSelection.ts) matched slices by CPU type only and refused when more than one matched. selectMachoSlice (src/native/AppleMachoSelection.ts) already ranked same-family slices by MACHO_ARCHITECTURE_POLICIES. That gave the repo two different rules for one decision.

Change and scope

  • selectPreferredMachoSlice in src/native/MachoArchitectureSelection.ts now owns the choice. It applies the existing per-family subtype order. Equal ranks keep FAT table order. The choice is ambiguous only when the winning base subtype (capability bits cleared) appears more than once, so a file with one arm64 slice and two arm64e slices still selects arm64. The loop is moved from selectMachoSlice, not rewritten.
  • selectMachoSlice and selectNativeImage both call it. selectMachoSlice now validates every FAT declaration before picking instead of interleaving the two. Every slice was already validated before it returned, so its results and errors are unchanged. machoSubtypeRank is no longer exported because nothing outside the module uses it.
  • This deliberately reverses one assertion from feat(native): inspect a universal Mach-O slice in Go and Rust metadata readers #6219: a file with arm64 (0x0) and arm64e (0x2) slices used to be refused as ambiguous. It now selects the arm64 slice. A duplicated winning subtype, such as two arm64 subtype 0 slices, is still refused with the same message. feat(native): inspect a universal Mach-O slice in Go and Rust metadata readers #6219 targeted Go and Rust binaries, which don't ship arm64e. The refusal started hitting stock macOS binaries once inspect_native_initializers adopted the same selector.
  • Same-family 32-bit arm slices with different subtypes, such as armv7 and armv7s, now resolve to the first one in table order instead of being refused. inspect_macho already behaves this way. Its lipo-based selectMachoArchitecture keeps the first equal-rank record, and selectArchitecture treats the choice as ambiguous only when another record has the same tool name after a .vN suffix is stripped.
  • selectNativeImage also returns a limitations line naming the same-family slices it skipped, with their subtypes and file offsets. inspect_native_initializers, inspect_go_binary and inspect_rust_binary append it to their own limitations. Files with one slice per family get no new line.
  • Docs: docs/guides/native/go-binaries.md, docs/rust-binaries.md and docs/guides/native/native-initializers.md now describe the preference order and the remaining refusal of a duplicated preferred subtype. The architecture schema description also mentions both.
  • Not included: selecting the arm64e slice of a file that also holds arm64. That needs a new selector value. I asked about it on [Bug] inspect-native-initializers can't read either arm64 slice of a universal Mach-O that holds both arm64 and arm64e #9792 and kept it out of this PR.
  • Remaining related cleanup: IL2CPP's private selectImage (src/il2cpp/Il2cppBinaryImage.ts) still refuses distinct ARM subtypes that both map to arm64. docs/il2cpp-metadata.md documents that refusal, and the selector has its own public vocabulary (amd64) and failure codes. feat(native): inspect a universal Mach-O slice in Go and Rust metadata readers #6219 also left it as a follow-up, so this PR doesn't touch it. Moving it onto selectPreferredMachoSlice would need the name mapping and the docs updated together. Separately, inspect_macho ranks lipo records (names plus parsed subtypes) in selectMachoArchitecture and judges ambiguity by tool name in NativeMachoInspection.selectArchitecture, rather than reading FAT declarations. It applies the same MACHO_ARCHITECTURE_POLICIES order, but it works on a different input representation, so this PR leaves it as is.
  • Same-base duplicates (for example two arm64 subtype-0 slices) are still refused on architecture with the existing message, as before this PR.

Contribution scope: not applicable. This is an ordinary fix inside existing boundaries.

Contract and boundary impact

  • Semantic owner and earliest changed stage: selectPreferredMachoSlice (MachoArchitectureSelection.ts) chooses the slice. Consumers are selectNativeImage (inspect_native_initializers, inspect_go_binary, inspect_rust_binary) and selectMachoSlice via parseMachoLayout. Of the parseMachoLayout callers, only dispatch metadata passes a universal file with an architecture. The Mach-O initializer parser and the IL2CPP layout reader receive slices that are already thinned, so their selection doesn't change. The duplicated selection loop in AppleMachoSelection.ts was removed.
  • Contribution scope fit and linked maintainer decision: not applicable.
  • CLI and MCP/tool-catalog contract: unchanged selector values and result shapes. The architecture description gains one sentence. Successful results on files with several same-family slices gain one limitations entry. Calls that used to fail with an architecture refusal on files with several slices of one family now succeed.
  • Provider, bridge, target-format, or platform compatibility: universal Mach-O only. Thin Mach-O, ELF and PE paths are unchanged.
  • Evidence, artifact, provenance, or reconstruction contract: unchanged. The selected slice's cpu_subtype, offset and container table digest are already reported.
  • Target selection, authority, upstream behavior, and cleanup details: not applicable.
  • Generated metadata, package, or installation impact: none.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated:

    • tests/boundary/mcp/nativeInitializersMcp.test.ts: an x86_64 + arm64 + arm64e universal file (the /usr/bin/file layout) built from the existing arm64 initializer fixture. architecture: "arm64" reads the subtype-0 slice at its file offset.
    • tests/boundary/mcp/universalSliceMcp.test.ts: with arm64e listed before arm64 in the FAT table, inspect_go_binary still selects the arm64 slice. Two subtype-0 slices are still refused with the existing "several arm64 slices" message. The arm64/arm64e refusal assertion was removed.
    • Both MCP cases also assert the new limitations line naming the unread arm64e slice and its offset.
    • src/native/MachoArchitectureSelection.test.ts (new): x86_64 is preferred over x86_64h in both table orders, 32-bit armv7s/armv7 keeps FAT table order, a duplicated winning subtype is ambiguous (including when only capability bits differ), one arm64 slice plus two arm64e slices still selects arm64, and an unknown family is absent.
    • Both new MCP cases fail on the base revision: with only src/ reverted, 2 tests fail and the rest pass.
  • Base reproduction (built CLI, main at 55b6d2ca2, macOS 26.5.2 arm64):

    $ rea inspect-native-initializers /usr/bin/file --architecture arm64 --format json
    {"code":"invalid_request","issues":[{"path":["architecture"],"reason":"invalid_value","message":"Universal Mach-O holds several arm64 slices (x86_64 (subtype 0x3), arm64 (subtype 0x0), arm64 (subtype 0x80000002)); no slice was selected."}]}
    $ rea inspect-native-initializers /usr/bin/file --architecture arm64e --format json
    {"code":"VALIDATION_ERROR","message":"REA could not read the command arguments. Check the named argument and its expected type, then try again."}
    

    /usr/bin/ktrace, /usr/bin/trace and /usr/bin/stz gave the same invalid_request.

  • User-visible CLI output after this change (built from this branch, same host). inspect-native-initializers <binary> --architecture <arch>, showing slice.file_offset, slice.cpu_subtype, coverage.status and coverage.total_entries:

    /usr/bin/file     arm64   ok {"off":180224,"sub":0,"cov":"complete","entries":0}
    /usr/bin/file     x86_64  ok {"off":16384,"sub":3,"cov":"complete","entries":0}
    /usr/bin/ktrace   arm64   ok {"off":131072,"sub":0,"cov":"complete","entries":0}
    /usr/bin/trace    arm64   ok {"off":196608,"sub":0,"cov":"complete","entries":0}
    /usr/bin/stz      arm64   ok {"off":147456,"sub":0,"cov":"complete","entries":0}
    /bin/ls           arm64   ok {"off":65536,"sub":2147483650,"cov":"complete","entries":0}
    TextEdit          arm64   ok {"off":376832,"sub":2147483650,"cov":"complete","entries":0}
    ShortcutDroplet   arm64   ok {"off":49152,"sub":0,"cov":"complete","entries":0}
    
    $ rea inspect-macho /usr/bin/file --format json   # normalized_result.selected_architecture
    {"name":"arm64","container_file_offset":180224}
    

    The /usr/bin/file result now ends with this limitation:

    Only the selected arm64 slice (arm64 (subtype 0x0) at file offset 180224) was read; this universal Mach-O also holds arm64 (subtype 0x80000002) at file offset 360448, which no architecture value selects.
    

    /usr/bin/file --architecture arm64 now reads offset 180224, the same slice inspect-macho selects. Two-slice files (/bin/ls, TextEdit, ShortcutDroplet) resolve to the same slice as before.

  • Remaining proof gaps: Go and Rust are covered by fixtures only. I had no real Go or Rust universal binary that carries both arm64 and arm64e. 32-bit arm multi-subtype ordering is covered by a selector unit test, not by a real binary. Linux and Windows hosts were not run locally.

Boundary regressions covered: malformed and truncated FAT input (existing damaged-slice and CPU-disagreement cases still pass), plus target identity (selected slice offset and subtype). No lifecycle stages are involved.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npm run check:fast: passed (typecheck and lint, after splitting the new Go case into its own test to stay under max-lines-per-function).
  • npm run test:focused -- <48 test files that reference the Mach-O slice selectors, parseMachoLayout, dispatch metadata, universal fixtures, initializers, the Go/Rust providers, or the architecture schema, plus tests/acceptance/tools/localToolContracts.test.ts and tests/boundary/mcp/serverIdentity.test.ts>: 49 files, 856 tests passed (on the final head).
  • npm run docs:check: passed.
  • npm run test:focused -- tests/boundary/mcp/universalSliceMcp.test.ts tests/boundary/mcp/nativeInitializersMcp.test.ts with src/ reverted to base: 2 failed (the new cases), 5 passed.
  • npm run build:cached, then the CLI runs shown above on macOS 26.5.2 arm64, Node 24.22.1.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. Inputs that used to fail now succeed. No previously successful call changes its result.
  • Real Hopper/Ghidra, browser, or OS coverage: real macOS system binaries on macOS 26.5.2 arm64. No provider is involved.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: read-only parsing. No new file access.

Author readiness

Human owner: @chordpli

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

inspect_native_initializers, inspect_go_binary and inspect_rust_binary matched universal Mach-O slices by CPU type only and refused files whose arm64 and arm64e slices share a CPU type, such as /usr/bin/file. Move the subtype-preference choice that inspect_macho already uses into selectPreferredMachoSlice and use it from both selectors, so arm64 resolves to one slice. Slices that share a base subtype are still refused.

Fixes morluto#9792
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T09:10:27.456951Z ad4d339 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8e9cc0914

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42baa4317b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docs/guides/native/go-binaries.md Resolved
When a universal Mach-O holds several slices of the selected CPU family, the metadata readers now name the slices they did not read in limitations. Correct the guides' description of the preference order, add selector unit cases for x86_64h and 32-bit ARM ordering, and mention the preference in the architecture schema.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77b203d6eb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docs/guides/native/go-binaries.md Resolved
Qualify the remaining refusal as a duplicated winning subtype, state that unlisted subtypes follow the listed ones, and say the limitation names the selected slice rather than a preferred one.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] inspect-native-initializers can't read either arm64 slice of a universal Mach-O that holds both arm64 and arm64e

1 participant