[GH-ISSUE #5688] [Bug] Hopper Mach-O preflight close failure replaces the primary read error and drops retry ownership #3439

Closed
opened 2026-10-31 00:40:30 +00:00 by bsakaguchi · 0 comments
Owner

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

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

At source revision 7dd6941393c852a3324cf01c2bd79a9dcff43728, resolveHopperMachOImage catches a source read failure and returns a BinaryTargetError, then unconditionally awaits file.close() in finally (src/hopper/HopperMachOImage.ts:168-174). A close rejection replaces the pending result. In a controlled source-boundary reproduction using an authored 4,160-byte FAT32 Mach-O, I injected EIO on the selected file's first descriptor read and a separate close error on that same descriptor. The call rejected with the close error instead of returning the typed read failure; the descriptor stayed open until the reproduction explicitly closed it.

Reproduction

I ran a standalone Node/tsx source-boundary reproduction against the exact source revision above. It writes a valid FAT32 container with one arm64 slice, captures the target's device/inode, and temporarily wraps FileHandle.prototype.read only for that identity. The wrapper raises a controlled EIO, captures that owned handle, and replaces its close method to raise a controlled close error. The harness restores the prototype and explicitly closes the captured descriptor in finally.

Observed output: readFaults: 1, closeFaults: 1, returnedResult: false, thrownIsCloseFailure: true, descriptorFdBeforeTeardown: 12, and descriptorFdAfterTeardown: -1. This is a controlled failure injection, not a real filesystem EIO or ordinary Hopper-provider reproduction. No Hopper process or target executable was launched.

Expected behavior

Return a typed analysis failure preserving the original source-read error as primary, attach the descriptor-close failure as cleanup, and retain the exact descriptor owner so cleanup can be retried. A successful retry should clear the caller-visible cleanup-incomplete state.

Impact and scope

The failure requires a read or validation error together with a close failure during Hopper's Mach-O preflight. Under that combination the caller loses the primary read reason, and baseline source does not retain a retryable owner for the descriptor. The reproduction establishes the failure at the resolver boundary, not its frequency in real filesystems. No target modification or target execution occurred. Frequency and production incidence are NOT_ENOUGH_INFO.

REA version

Source commit 7dd6941393c852a3324cf01c2bd79a9dcff43728; no packaged REA version was run.

Runtime and platform

Node.js 24.18.0; macOS 26.3 build 25D125; arm64. The failure was injected at the file-handle boundary.

Target and artifact context

Authored 4,160-byte FAT32 Mach-O test fixture containing one arm64 slice. It is inert test data, not a shipped application or Hopper database.

Interface

Other — direct source-boundary invocation of resolveHopperMachOImage.

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

The reproduction script and raw JSON are available as separate artifacts:

  • rea-hopper-preflight-baseline-repro.mts, SHA-256 ac7bd591c2c354906fc9a1c2ac4ed8162dc058891662104eb92f151cf8361d26
  • rea-hopper-preflight-baseline-node24.json, SHA-256 5781a6260bd6e21e6ee1330d1ac76468062b18632b17b88ef9d10c655c9c32a3

The result supports the combined-read/close failure claim. The file identity guard prevents the seam from affecting unrelated descriptors, and the harness restores the prototype and verifies the descriptor is closed after teardown.

Typed outcome or recovery expected

A typed BinaryTargetError retaining the original read EIO, with cleanup metadata for the failed descriptor close and retryable ownership. No resolver result should be replaced by an untyped close rejection.

Analysis or suspected cause

Source-derived: the catch returns invalid(...), but JavaScript runs the finally before returning. If file.close() rejects, it replaces the return. The owner is a raw FileHandle; no retained resource scope exists at this boundary in the baseline source. This report concerns preflight ownership and is separate from #5607, which concerns duplicate Objective-C class rows. Closed #3072 concerned a different FAT slice-selection rule. Search results for resolveHopperMachOImage, Hopper descriptor cleanup, and Hopper FAT close failures returned no matching open issue or pull request; #4227 is a broad lifecycle audit tracker rather than a report of this defect.

Additional context

A frozen, uncommitted source candidate at the same base revision was checked with the same injected read/close failures through the existing filesystem option. It returned a typed error preserving the injected read failure, retained the descriptor under a named cleanup owner, and cleared cleanup-incomplete after a successful retry. This verifies the proposed recovery path at source boundary; it is not a public MCP/CLI or real-filesystem reproduction.

Originally created by @morluto on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5688 ### 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 At source revision `7dd6941393c852a3324cf01c2bd79a9dcff43728`, `resolveHopperMachOImage` catches a source read failure and returns a `BinaryTargetError`, then unconditionally awaits `file.close()` in `finally` (`src/hopper/HopperMachOImage.ts:168-174`). A close rejection replaces the pending result. In a controlled source-boundary reproduction using an authored 4,160-byte FAT32 Mach-O, I injected `EIO` on the selected file's first descriptor read and a separate close error on that same descriptor. The call rejected with the close error instead of returning the typed read failure; the descriptor stayed open until the reproduction explicitly closed it. ### Reproduction I ran a standalone Node/tsx source-boundary reproduction against the exact source revision above. It writes a valid FAT32 container with one arm64 slice, captures the target's device/inode, and temporarily wraps `FileHandle.prototype.read` only for that identity. The wrapper raises a controlled `EIO`, captures that owned handle, and replaces its close method to raise a controlled close error. The harness restores the prototype and explicitly closes the captured descriptor in `finally`. Observed output: `readFaults: 1`, `closeFaults: 1`, `returnedResult: false`, `thrownIsCloseFailure: true`, `descriptorFdBeforeTeardown: 12`, and `descriptorFdAfterTeardown: -1`. This is a controlled failure injection, not a real filesystem EIO or ordinary Hopper-provider reproduction. No Hopper process or target executable was launched. ### Expected behavior Return a typed analysis failure preserving the original source-read error as primary, attach the descriptor-close failure as cleanup, and retain the exact descriptor owner so cleanup can be retried. A successful retry should clear the caller-visible cleanup-incomplete state. ### Impact and scope The failure requires a read or validation error together with a close failure during Hopper's Mach-O preflight. Under that combination the caller loses the primary read reason, and baseline source does not retain a retryable owner for the descriptor. The reproduction establishes the failure at the resolver boundary, not its frequency in real filesystems. No target modification or target execution occurred. Frequency and production incidence are `NOT_ENOUGH_INFO`. ### REA version Source commit `7dd6941393c852a3324cf01c2bd79a9dcff43728`; no packaged REA version was run. ### Runtime and platform Node.js 24.18.0; macOS 26.3 build 25D125; arm64. The failure was injected at the file-handle boundary. ### Target and artifact context Authored 4,160-byte FAT32 Mach-O test fixture containing one arm64 slice. It is inert test data, not a shipped application or Hopper database. ### Interface Other — direct source-boundary invocation of `resolveHopperMachOImage`. ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence The reproduction script and raw JSON are available as separate artifacts: - `rea-hopper-preflight-baseline-repro.mts`, SHA-256 `ac7bd591c2c354906fc9a1c2ac4ed8162dc058891662104eb92f151cf8361d26` - `rea-hopper-preflight-baseline-node24.json`, SHA-256 `5781a6260bd6e21e6ee1330d1ac76468062b18632b17b88ef9d10c655c9c32a3` The result supports the combined-read/close failure claim. The file identity guard prevents the seam from affecting unrelated descriptors, and the harness restores the prototype and verifies the descriptor is closed after teardown. ### Typed outcome or recovery expected A typed `BinaryTargetError` retaining the original read `EIO`, with cleanup metadata for the failed descriptor close and retryable ownership. No resolver result should be replaced by an untyped close rejection. ### Analysis or suspected cause Source-derived: the `catch` returns `invalid(...)`, but JavaScript runs the `finally` before returning. If `file.close()` rejects, it replaces the return. The owner is a raw `FileHandle`; no retained resource scope exists at this boundary in the baseline source. This report concerns preflight ownership and is separate from #5607, which concerns duplicate Objective-C class rows. Closed #3072 concerned a different FAT slice-selection rule. Search results for `resolveHopperMachOImage`, Hopper descriptor cleanup, and Hopper FAT close failures returned no matching open issue or pull request; #4227 is a broad lifecycle audit tracker rather than a report of this defect. ### Additional context A frozen, uncommitted source candidate at the same base revision was checked with the same injected read/close failures through the existing filesystem option. It returned a typed error preserving the injected read failure, retained the descriptor under a named cleanup owner, and cleared cleanup-incomplete after a successful retry. This verifies the proposed recovery path at source boundary; it is not a public MCP/CLI or real-filesystem reproduction.
bsakaguchi 2026-10-31 00:40:30 +00:00
  • closed this issue
  • added the
    bug
    triage
    labels
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#3439
No description provided.