[GH-ISSUE #7651] [Bug] Dispatch metadata loses the unsupported-width reason when its symbol fallback fails #4574

Open
opened 2026-10-31 00:50:55 +00:00 by bsakaguchi · 0 comments
Owner

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

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

For minimal 32-bit armv7 and i386 Mach-O files, node scripts/rea.mjs inspect-native-dispatch-metadata <file> --format json exits 1 and returns a provider_unavailable error for list_names. The complete response contains provider setup diagnostics but omits the native reader's already-established reason that it cannot decode 32-bit metadata.

An independent call to the built byte reader on the same fixture bytes returned AnalysisCapabilityUnavailableError with the specific unsupported-width reason. The public command loses that information when its attempted symbol inventory also fails. Both CLI stderr streams were empty.

Reproduction

  1. Use the source revision below with its existing built artifacts and locked dependencies. Select the repository's pinned Node.js 24.22.1 runtime.
  2. Run this block from the checkout root. It builds two 28-byte headers, isolates product state, and prevents package runners or a real Hopper launcher from being used. Its assertions characterize the current defective output; they are not a repaired-behavior test.
python3 - <<'PY'
import json, os, pathlib, struct, subprocess, tempfile

node = subprocess.check_output(["node", "-p", "process.execPath"], text=True).strip()
with tempfile.TemporaryDirectory(prefix="rea-width-verification-") as work:
    root = pathlib.Path(work)
    env = {"PATH": f"{root / 'bin'}:{pathlib.Path(node).parent}:/usr/bin:/bin", "NO_COLOR": "1"}
    for key in ("HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "TMPDIR"):
        directory = root / key.lower()
        directory.mkdir()
        env[key] = str(directory)
    (root / "bin").mkdir()
    for name in ("npx", "npm", "bun"):
        launcher = root / "bin" / name
        launcher.write_text('#!/bin/sh\necho "$0 $*" >> "$HOME/fixture-launches.log"\nexit 127\n')
        launcher.chmod(0o755)
    env["HOPPER_LAUNCHER_PATH"] = str(root / "missing-hopper")
    for name, cpu, subtype in (("armv7", 12, 9), ("i386", 7, 3)):
        binary = root / name
        binary.write_bytes(struct.pack("<7I", 0xfeedface, cpu, subtype, 2, 0, 0, 0))
        command = [node, "scripts/rea.mjs", "inspect-native-dispatch-metadata", str(binary), "--format", "json"]
        result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=30)
        error = json.loads(result.stdout)
        print(name, "bytes=", binary.stat().st_size, "exit=", result.returncode)
        print(json.dumps(error))
        assert result.returncode == 1 and error["code"] == "provider_unavailable"
        assert error["details"]["operation"] == "list_names"
        assert "Unsupported 32-bit" not in result.stdout
        assert result.stderr == ""
    print("fixture launch attempts:", (pathlib.Path(env["HOME"]) / "fixture-launches.log").exists())
PY

The two-fixture block reproduced twice. An earlier matrix also reproduced twice for each architecture at both 28 and 32 bytes: eight failures in eight calls. The padding does not change the missing diagnostic.

Expected behavior

Keep the reader's unsupported-width explanation visible in the final command failure, alongside the symbol fallback's provider readiness diagnostics. The exact nesting and sentence format are not prescribed.

The repository's error-boundary invariant requires preserving meaningful failure reasons. Its native investigation guide permits symbol-only fallback and limits the byte reader to little-endian 64-bit metadata. Those contracts support retaining both established facts: direct byte metadata is unsupported for this image, and the alternative symbol inventory cannot currently run. They do not require adding 32-bit decoding or removing fallback.

Impact and scope

P2: the caller cannot distinguish the known byte-reader coverage limit from the unavailable alternative analysis. The returned recovery describes provider readiness without explaining that configuring a provider would enable a different, narrower source of facts. This is lost diagnostic information; no successful metadata result, target modification, or security-boundary crossing was observed.

Observed scope is these synthetic armv7/i386 thin files through the built CLI on macOS. Real-provider fallback success, MCP delivery, arm64_32, other hosts, and naturally occurring frequency were not tested. No REA invocation that restores the missing reason was verified.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0.
Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; darwin arm64. Ghidra, IDA and Kuna were unconfigured in the final reproduction; Hopper's selected launcher was deliberately nonexistent. The npx/npm/bun fixture launch log was absent after both calls. No real analysis provider was run.

Target and artifact context

Synthetic thin little-endian MH_EXECUTE headers: CPU type 12/subtype 9 for armv7, and CPU type 7/subtype 3 for i386. Each has zero load commands and a 28-byte header. Four-byte-padded controls produced the same result. The fixtures contain no Objective-C/Swift records and were never executed; they isolate format admission and diagnostic preservation.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Selected fields from the complete JSON produced by the reproduction:

armv7: bytes=28, exit=1
  code: provider_unavailable
  category: unavailable
  retryable: true
  details.operation: list_names
  details.selection_reason: provider_unavailable
  message: Provider selection failed local readiness checks. No target-support refusal was reported. Review the local provider diagnostics or run `rea doctor`, then retry.

i386: bytes=28, exit=1
  code: provider_unavailable
  category: unavailable
  retryable: true
  details.operation: list_names
  details.selection_reason: provider_unavailable
  message: The selected analysis provider is unavailable. Review the reported local diagnostics or run `rea doctor`, then retry.

The armv7 rejections all have origin: availability. The i386 result additionally retains Kuna's origin: target / architecture_unsupported rejection. Neither complete result contains the native reader's width refusal.

The independently executed compiled-reader probe called inspectAppleDispatchMetadata and mapped its thrown failure through appleDispatchMetadataFailure. It returned these reasons, each with cleanupIncomplete: false:

armv7: Unsupported 32-bit Mach-O metadata for requested CPU type arm
i386: Unsupported 32-bit Mach-O metadata for requested CPU type x86

Contract and failure locators at the stated baseline:

  • AGENTS.md:33: meaningful failure reasons must survive error boundaries.
  • docs/tool-design.md:59-68: relevant limitations and uncertainty belong with the returned facts.
  • docs/native-investigation.md:729-738,837-850: byte-reader scope and documented symbol alternative.
  • src/native/AppleMachoSelection.ts:194-200: correct CPU matching followed by the recognized 32-bit refusal.
  • src/native/AppleDispatchMetadata.ts:442-454: specific refusal retained in the capability error.
  • src/application/CapabilityFallback.ts:5-10: clean capability errors permit alternatives.
  • src/application/native/NativeDispatchMetadataInspection.ts:75-86: failed symbol execution is returned without the earlier error.
  • src/domain/providerSelectionError.ts:91-106: availability-only versus mixed provider rejection wording.

Typed outcome or recovery expected

A failed fallback should preserve the direct operation's unavailable reason and attribution as context. Keeping provider_unavailable for the failed symbol alternative can be consistent with that requirement. A restoration check would replay both fixtures and require their specific reader refusal as well as the provider diagnostics, while preserving intentional symbol-only fallback behavior.

Analysis or suspected cause

Source-derived: the shared dispatch workflow holds the direct error locally, then returns the symbol attempt's error unchanged. The first reason is available at that boundary and is not copied into the final failure. Provider selection subsequently describes only its own candidate rejection set, so its wording cannot recover the omitted reader observation.

Additional context

Merged PR #7220 intentionally introduced the availability-only sentence. This report does not challenge that provider-level wording or claim that an availability rejection establishes target support. It concerns the separate reader refusal lost before that error reaches the caller.

Open #7421 covers empty-path resolution; both paths here are nonempty regular files. Closed #5840 repaired the reason attached to successful get_objc_classes fallback coverage, and closed #5997 covers filesystem failure classification. Neither covers this command's failed symbol retry. Closed #1502 repaired FAT slice selection; this reproduction uses thin images and independently confirms the reader already selects and refuses the correct CPU family. No regression of those repaired boundaries is asserted.

Read-only searches covered open/closed issues, open/closed/merged PRs, and comments using dispatch, 32-bit, unsupported-width, provider-unavailable, reader-refusal and fallback terms. No report of this same failed-fallback loss was found.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Both public CLI calls omit an independently confirmed target-format reason and return only the alternative provider-selection failure, losing information needed to interpret coverage and recovery.
  • Workaround status: unverified
  • Workaround basis: No tested REA call returns the lost reason in the final response; independent inspection can identify the image width, but real-provider fallback and its resulting coverage were not exercised.
Originally created by @coygeek on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7651 ### 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 For minimal 32-bit armv7 and i386 Mach-O files, `node scripts/rea.mjs inspect-native-dispatch-metadata <file> --format json` exits 1 and returns a `provider_unavailable` error for `list_names`. The complete response contains provider setup diagnostics but omits the native reader's already-established reason that it cannot decode 32-bit metadata. An independent call to the built byte reader on the same fixture bytes returned `AnalysisCapabilityUnavailableError` with the specific unsupported-width reason. The public command loses that information when its attempted symbol inventory also fails. Both CLI stderr streams were empty. ### Reproduction 1. Use the source revision below with its existing built artifacts and locked dependencies. Select the repository's pinned Node.js 24.22.1 runtime. 2. Run this block from the checkout root. It builds two 28-byte headers, isolates product state, and prevents package runners or a real Hopper launcher from being used. Its assertions characterize the current defective output; they are not a repaired-behavior test. ```sh python3 - <<'PY' import json, os, pathlib, struct, subprocess, tempfile node = subprocess.check_output(["node", "-p", "process.execPath"], text=True).strip() with tempfile.TemporaryDirectory(prefix="rea-width-verification-") as work: root = pathlib.Path(work) env = {"PATH": f"{root / 'bin'}:{pathlib.Path(node).parent}:/usr/bin:/bin", "NO_COLOR": "1"} for key in ("HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "TMPDIR"): directory = root / key.lower() directory.mkdir() env[key] = str(directory) (root / "bin").mkdir() for name in ("npx", "npm", "bun"): launcher = root / "bin" / name launcher.write_text('#!/bin/sh\necho "$0 $*" >> "$HOME/fixture-launches.log"\nexit 127\n') launcher.chmod(0o755) env["HOPPER_LAUNCHER_PATH"] = str(root / "missing-hopper") for name, cpu, subtype in (("armv7", 12, 9), ("i386", 7, 3)): binary = root / name binary.write_bytes(struct.pack("<7I", 0xfeedface, cpu, subtype, 2, 0, 0, 0)) command = [node, "scripts/rea.mjs", "inspect-native-dispatch-metadata", str(binary), "--format", "json"] result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=30) error = json.loads(result.stdout) print(name, "bytes=", binary.stat().st_size, "exit=", result.returncode) print(json.dumps(error)) assert result.returncode == 1 and error["code"] == "provider_unavailable" assert error["details"]["operation"] == "list_names" assert "Unsupported 32-bit" not in result.stdout assert result.stderr == "" print("fixture launch attempts:", (pathlib.Path(env["HOME"]) / "fixture-launches.log").exists()) PY ``` The two-fixture block reproduced twice. An earlier matrix also reproduced twice for each architecture at both 28 and 32 bytes: eight failures in eight calls. The padding does not change the missing diagnostic. ### Expected behavior Keep the reader's unsupported-width explanation visible in the final command failure, alongside the symbol fallback's provider readiness diagnostics. The exact nesting and sentence format are not prescribed. The repository's error-boundary invariant requires preserving meaningful failure reasons. Its native investigation guide permits symbol-only fallback and limits the byte reader to little-endian 64-bit metadata. Those contracts support retaining both established facts: direct byte metadata is unsupported for this image, and the alternative symbol inventory cannot currently run. They do not require adding 32-bit decoding or removing fallback. ### Impact and scope P2: the caller cannot distinguish the known byte-reader coverage limit from the unavailable alternative analysis. The returned recovery describes provider readiness without explaining that configuring a provider would enable a different, narrower source of facts. This is lost diagnostic information; no successful metadata result, target modification, or security-boundary crossing was observed. Observed scope is these synthetic armv7/i386 thin files through the built CLI on macOS. Real-provider fallback success, MCP delivery, arm64_32, other hosts, and naturally occurring frequency were not tested. No REA invocation that restores the missing reason was verified. ### REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`; `darwin arm64`. Ghidra, IDA and Kuna were unconfigured in the final reproduction; Hopper's selected launcher was deliberately nonexistent. The npx/npm/bun fixture launch log was absent after both calls. No real analysis provider was run. ### Target and artifact context Synthetic thin little-endian MH_EXECUTE headers: CPU type 12/subtype 9 for armv7, and CPU type 7/subtype 3 for i386. Each has zero load commands and a 28-byte header. Four-byte-padded controls produced the same result. The fixtures contain no Objective-C/Swift records and were never executed; they isolate format admission and diagnostic preservation. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Selected fields from the complete JSON produced by the reproduction: ```text armv7: bytes=28, exit=1 code: provider_unavailable category: unavailable retryable: true details.operation: list_names details.selection_reason: provider_unavailable message: Provider selection failed local readiness checks. No target-support refusal was reported. Review the local provider diagnostics or run `rea doctor`, then retry. i386: bytes=28, exit=1 code: provider_unavailable category: unavailable retryable: true details.operation: list_names details.selection_reason: provider_unavailable message: The selected analysis provider is unavailable. Review the reported local diagnostics or run `rea doctor`, then retry. ``` The armv7 rejections all have `origin: availability`. The i386 result additionally retains Kuna's `origin: target` / `architecture_unsupported` rejection. Neither complete result contains the native reader's width refusal. The independently executed compiled-reader probe called `inspectAppleDispatchMetadata` and mapped its thrown failure through `appleDispatchMetadataFailure`. It returned these reasons, each with `cleanupIncomplete: false`: ```text armv7: Unsupported 32-bit Mach-O metadata for requested CPU type arm i386: Unsupported 32-bit Mach-O metadata for requested CPU type x86 ``` Contract and failure locators at the stated baseline: - `AGENTS.md:33`: meaningful failure reasons must survive error boundaries. - `docs/tool-design.md:59-68`: relevant limitations and uncertainty belong with the returned facts. - `docs/native-investigation.md:729-738,837-850`: byte-reader scope and documented symbol alternative. - `src/native/AppleMachoSelection.ts:194-200`: correct CPU matching followed by the recognized 32-bit refusal. - `src/native/AppleDispatchMetadata.ts:442-454`: specific refusal retained in the capability error. - `src/application/CapabilityFallback.ts:5-10`: clean capability errors permit alternatives. - `src/application/native/NativeDispatchMetadataInspection.ts:75-86`: failed symbol execution is returned without the earlier error. - `src/domain/providerSelectionError.ts:91-106`: availability-only versus mixed provider rejection wording. ### Typed outcome or recovery expected A failed fallback should preserve the direct operation's unavailable reason and attribution as context. Keeping `provider_unavailable` for the failed symbol alternative can be consistent with that requirement. A restoration check would replay both fixtures and require their specific reader refusal as well as the provider diagnostics, while preserving intentional symbol-only fallback behavior. ### Analysis or suspected cause Source-derived: the shared dispatch workflow holds the direct error locally, then returns the symbol attempt's error unchanged. The first reason is available at that boundary and is not copied into the final failure. Provider selection subsequently describes only its own candidate rejection set, so its wording cannot recover the omitted reader observation. ### Additional context Merged [PR #7220](https://github.com/morluto/rea/pull/7220) intentionally introduced the availability-only sentence. This report does not challenge that provider-level wording or claim that an availability rejection establishes target support. It concerns the separate reader refusal lost before that error reaches the caller. Open [#7421](https://github.com/morluto/rea/issues/7421) covers empty-path resolution; both paths here are nonempty regular files. Closed [#5840](https://github.com/morluto/rea/issues/5840) repaired the reason attached to successful `get_objc_classes` fallback coverage, and closed [#5997](https://github.com/morluto/rea/issues/5997) covers filesystem failure classification. Neither covers this command's failed symbol retry. Closed [#1502](https://github.com/morluto/rea/issues/1502) repaired FAT slice selection; this reproduction uses thin images and independently confirms the reader already selects and refuses the correct CPU family. No regression of those repaired boundaries is asserted. Read-only searches covered open/closed issues, open/closed/merged PRs, and comments using dispatch, 32-bit, unsupported-width, provider-unavailable, reader-refusal and fallback terms. No report of this same failed-fallback loss was found. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Both public CLI calls omit an independently confirmed target-format reason and return only the alternative provider-selection failure, losing information needed to interpret coverage and recovery. - Workaround status: unverified - Workaround basis: No tested REA call returns the lost reason in the final response; independent inspection can identify the image width, but real-provider fallback and its resulting coverage were not exercised.
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#4574
No description provided.