[GH-ISSUE #4658] [Bug] Electron fuse inspection loses failed-close ownership and replaces read failures #2427

Closed
opened 2026-10-17 21:04:30 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

  • I searched existing issues and pull requests.
  • The report contains no private target data or credentials.

Area

Evidence, artifacts, or reconstruction

Observed behavior

Static Electron fuse inspection loses ownership of an admitted file descriptor when its final close() rejects. The response classifies the close error as a filesystem read permission failure and does not report incomplete cleanup. If reading already failed, the close rejection also replaces that primary error.

I reproduced this on main 150e996a86f2bec4982ba87f37bf502a8d5e8214, using actual Node FileHandle instances with controlled read/close rejections. Two requests over the same real SDK stdio MCP connection left two actual descriptors open until the probe explicitly closed them. This is a controlled failure-path reproduction, not a claim that an ordinary Electron installation naturally causes close failures or that descriptors survive process exit.

Reproduction

Build the package, create a small ELF input, then preload a fault only for this selected file:

npm run build:cached
node -e 'require("node:fs").writeFileSync("/tmp/rea-fuse-owner.elf",Buffer.from([0x7f,0x45,0x4c,0x46,0,0,0,0]))'

Save the following as /tmp/rea-fuse-close-fault.mjs:

import fs from "node:fs";
import { syncBuiltinESMExports } from "node:module";
const originalOpen = fs.promises.open;
const retained = [];
fs.promises.open = async function (path, ...args) {
  const handle = await originalOpen.call(this, path, ...args);
  if (String(path) !== "/tmp/rea-fuse-owner.elf") return handle;
  retained.push(handle);
  // Remove this assignment to reproduce close failure after successful reading.
  handle.read = async () => {
    throw Object.assign(new Error("physical read failure"), { code: "EIO" });
  };
  handle.close = async () => {
    throw Object.assign(new Error("close rejected; descriptor still live"), {
      code: "EACCES",
    });
  };
  return handle;
};
syncBuiltinESMExports();
process.on("exit", () => {
  for (const handle of retained) {
    try {
      fs.fstatSync(handle.fd);
      process.stderr.write(`still-open descriptor: ${handle.fd}\n`);
      fs.closeSync(handle.fd); // Probe-owned final cleanup.
    } catch {}
  }
});
node --import /tmp/rea-fuse-close-fault.mjs scripts/rea.mjs inspect-electron-fuses /tmp/rea-fuse-owner.elf --json

The CLI exits 1 with code: "access_denied", details.boundary: "filesystem-read", and remediation to check read permissions. The original EIO is lost, and the exit audit still finds the descriptor open. Removing the injected read rejection still produces the same read-permission classification for a close-only failure.

I also ran the actual package MCP dispatcher with the same preload and sent two inspect_electron_fuses calls through one SDK client. Both returned isError: true with this classification; the audit found two still-open descriptors. The probe then explicitly closed those exact descriptors.

Expected behavior

The admitted descriptor should remain associated with a retryable cleanup owner after a rejected close. Responses should expose incomplete cleanup and the actual owned resource. A preceding physical read failure should retain its identity and receive the cleanup dependency, rather than being replaced by a close error or a later abort.

Impact and scope

This affects failure diagnosis and resource recovery for static fuse inspection, especially a long-running MCP server. Normal inspection of a valid 70-byte Mach-O fuse fixture returned the expected SHA-256 and closed its descriptor. A pre-aborted producer call opened no descriptors and retained the caller's exact cancellation reason. These controls passed.

The reader is also used by active Electron fuse preflight; its catch currently allows unknown read failures to proceed. That consumer implication is source-derived only; I have not launched Electron under the injected failure.

REA version

Source main 150e996a86f2bec4982ba87f37bf502a8d5e8214.

Runtime and platform

Node 24.18.0, macOS arm64. Static constructed input only; Windows/Linux host behavior and actual Electron launch were not tested.

Interface

Public package CLI (scripts/rea.mjs) and SDK stdio MCP (scripts/rea.mjs mcp); direct compiled provider controls additionally preserve exact error identity and inspect the real descriptors.

Earliest failure stage

Cleanup, cancellation, or lifecycle

Analysis

ElectronFuseReader.ts closes the raw admitted handle in finally, without the shared artifact cleanup owner. ElectronFuseProvider.ts then classifies the replacement error. The provider currently exposes no cleanup retry method.

I checked the actual related patches in #3587 (framework symlinks), #3605 (disabled-inspector-fuse preflight), and #1960 (reverse-engineering lifecycle). They do not repair this admitted descriptor's failed-close ownership.

Prepared fork fix (2026-10-17)

A normal commit and fork push are available at dbb7db16. The same issue was freshly replayed through the compiled CLI on current main cc2e18a265198492dff1587a343bfb802d8c591b after the original report.

The fix uses the existing artifact owner, retains the exact failed descriptor and primary/partial outcome, joins active preflight finalization, and binds CLI/MCP shutdown and retry to that owner. Actual CLI and SDK stdio controls confirm normal output, blocked repeated acquisition after close denial, and recovery on the same MCP connection.

Focused validation: 28 distinct files, 292 PASS / 1 conditional skip (unchanged v3 inputs plus three affected final-binding files rerun with 26 PASS, overlap not double-counted); final check and guards, docs plus 5 fresh README checks, and discovery of all 1,462 files passed. Independent Standards/Spec review passed. Natural OS-induced close failures, faulted real Electron and Windows/Linux host faults were not run.

This is a prepared fork branch, not a new upstream PR or upstream CI result. Ordinary upstream PR creation is still denied for an unknown policy; no unchanged create retry or bypass was attempted.

Originally created by @xu91102 on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4658 ### Before submitting - [x] I searched existing issues and pull requests. - [x] The report contains no private target data or credentials. ### Area Evidence, artifacts, or reconstruction ### Observed behavior Static Electron fuse inspection loses ownership of an admitted file descriptor when its final `close()` rejects. The response classifies the close error as a filesystem **read** permission failure and does not report incomplete cleanup. If reading already failed, the close rejection also replaces that primary error. I reproduced this on main `150e996a86f2bec4982ba87f37bf502a8d5e8214`, using actual Node `FileHandle` instances with controlled read/close rejections. Two requests over the same real SDK stdio MCP connection left two actual descriptors open until the probe explicitly closed them. This is a controlled failure-path reproduction, not a claim that an ordinary Electron installation naturally causes close failures or that descriptors survive process exit. ### Reproduction Build the package, create a small ELF input, then preload a fault only for this selected file: ```sh npm run build:cached node -e 'require("node:fs").writeFileSync("/tmp/rea-fuse-owner.elf",Buffer.from([0x7f,0x45,0x4c,0x46,0,0,0,0]))' ``` Save the following as `/tmp/rea-fuse-close-fault.mjs`: ```js import fs from "node:fs"; import { syncBuiltinESMExports } from "node:module"; const originalOpen = fs.promises.open; const retained = []; fs.promises.open = async function (path, ...args) { const handle = await originalOpen.call(this, path, ...args); if (String(path) !== "/tmp/rea-fuse-owner.elf") return handle; retained.push(handle); // Remove this assignment to reproduce close failure after successful reading. handle.read = async () => { throw Object.assign(new Error("physical read failure"), { code: "EIO" }); }; handle.close = async () => { throw Object.assign(new Error("close rejected; descriptor still live"), { code: "EACCES", }); }; return handle; }; syncBuiltinESMExports(); process.on("exit", () => { for (const handle of retained) { try { fs.fstatSync(handle.fd); process.stderr.write(`still-open descriptor: ${handle.fd}\n`); fs.closeSync(handle.fd); // Probe-owned final cleanup. } catch {} } }); ``` ```sh node --import /tmp/rea-fuse-close-fault.mjs scripts/rea.mjs inspect-electron-fuses /tmp/rea-fuse-owner.elf --json ``` The CLI exits 1 with `code: "access_denied"`, `details.boundary: "filesystem-read"`, and remediation to check read permissions. The original `EIO` is lost, and the exit audit still finds the descriptor open. Removing the injected read rejection still produces the same read-permission classification for a close-only failure. I also ran the actual package MCP dispatcher with the same preload and sent two `inspect_electron_fuses` calls through one SDK client. Both returned `isError: true` with this classification; the audit found two still-open descriptors. The probe then explicitly closed those exact descriptors. ### Expected behavior The admitted descriptor should remain associated with a retryable cleanup owner after a rejected close. Responses should expose incomplete cleanup and the actual owned resource. A preceding physical read failure should retain its identity and receive the cleanup dependency, rather than being replaced by a close error or a later abort. ### Impact and scope This affects failure diagnosis and resource recovery for static fuse inspection, especially a long-running MCP server. Normal inspection of a valid 70-byte Mach-O fuse fixture returned the expected SHA-256 and closed its descriptor. A pre-aborted producer call opened no descriptors and retained the caller's exact cancellation reason. These controls passed. The reader is also used by active Electron fuse preflight; its catch currently allows unknown read failures to proceed. That consumer implication is source-derived only; I have not launched Electron under the injected failure. ### REA version Source main `150e996a86f2bec4982ba87f37bf502a8d5e8214`. ### Runtime and platform Node 24.18.0, macOS arm64. Static constructed input only; Windows/Linux host behavior and actual Electron launch were not tested. ### Interface Public package CLI (`scripts/rea.mjs`) and SDK stdio MCP (`scripts/rea.mjs mcp`); direct compiled provider controls additionally preserve exact error identity and inspect the real descriptors. ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Analysis [`ElectronFuseReader.ts`](https://github.com/morluto/rea/blob/150e996a86f2bec4982ba87f37bf502a8d5e8214/src/electron/ElectronFuseReader.ts) closes the raw admitted handle in `finally`, without the shared artifact cleanup owner. [`ElectronFuseProvider.ts`](https://github.com/morluto/rea/blob/150e996a86f2bec4982ba87f37bf502a8d5e8214/src/electron/ElectronFuseProvider.ts) then classifies the replacement error. The provider currently exposes no cleanup retry method. I checked the actual related patches in #3587 (framework symlinks), #3605 (disabled-inspector-fuse preflight), and #1960 (reverse-engineering lifecycle). They do not repair this admitted descriptor's failed-close ownership. ## Prepared fork fix (2026-10-17) A normal commit and fork push are available at [dbb7db16](https://github.com/xu91102/rea/commit/dbb7db16ee49ce8cdf823f5d8c9e5033c7299601). The same issue was freshly replayed through the compiled CLI on current main `cc2e18a265198492dff1587a343bfb802d8c591b` after the original report. The fix uses the existing artifact owner, retains the exact failed descriptor and primary/partial outcome, joins active preflight finalization, and binds CLI/MCP shutdown and retry to that owner. Actual CLI and SDK stdio controls confirm normal output, blocked repeated acquisition after close denial, and recovery on the same MCP connection. Focused validation: 28 distinct files, 292 PASS / 1 conditional skip (unchanged v3 inputs plus three affected final-binding files rerun with 26 PASS, overlap not double-counted); final check and guards, docs plus 5 fresh README checks, and discovery of all 1,462 files passed. Independent Standards/Spec review passed. Natural OS-induced close failures, faulted real Electron and Windows/Linux host faults were not run. This is a prepared fork branch, not a new upstream PR or upstream CI result. Ordinary upstream PR creation is still denied for an unknown policy; no unchanged create retry or bypass was attempted.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Reopening after auditing current main bf54c4626. The integrated #4668 patch preserves failed-close ownership in an error cause, but the remaining lifecycle repair linked in this report (fork dbb7db16) is not integrated: ElectronFusePort/Service expose no close, CLI does not finish the service, MCP does not close it, and active preflight can swallow the retained cleanup failure. Source-derived for these adapter implications; no faulted Electron launch was performed.

Five new source controls using actual admitted FileHandles fail on this main: a physical EACCES read followed by successful final close and late abort becomes cancellation; completed bundle fuse observations after successful-close cancellation lose partial Evidence; pre-aborted service calls lose exact null/object/default cancellation reasons (3 cases). These are controlled boundary injections, not natural filesystem failures.

I am adapting dbb7db16 and checking the complete owner/retry/finalization path, including the current architecture facts added for #4786.

<!-- gh-comment-id:6234309964 --> @morluto commented on GitHub (Oct 17, 2026): Reopening after auditing current main bf54c4626. The integrated #4668 patch preserves failed-close ownership in an error cause, but the remaining lifecycle repair linked in this report (fork dbb7db16) is not integrated: ElectronFusePort/Service expose no close, CLI does not finish the service, MCP does not close it, and active preflight can swallow the retained cleanup failure. Source-derived for these adapter implications; no faulted Electron launch was performed. Five new source controls using actual admitted FileHandles fail on this main: a physical EACCES read followed by successful final close and late abort becomes cancellation; completed bundle fuse observations after successful-close cancellation lose partial Evidence; pre-aborted service calls lose exact null/object/default cancellation reasons (3 cases). These are controlled boundary injections, not natural filesystem failures. I am adapting dbb7db16 and checking the complete owner/retry/finalization path, including the current architecture facts added for #4786.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Completed the remaining fuse-descriptor lifecycle repair on main in f9de41371, adapting @xu91102’s prepared dbb7db16 with co-author credit.

The exact descriptor lease now survives error translation, blocked admission, retry, CLI finalization and MCP shutdown. Active preflight cannot swallow its cleanup failure or outlive final close. Active-provider shutdown cancels and joins admitted captures and gates launch. Physical read failures keep precedence; exact caller reasons and completed canonical Evidence survive close and the later reader/provider handoff. One Evidence builder preserves selected/resolved paths, architecture, digests, fuses and locations. Mixed descriptor recovery clears only confirmed owners.

Evidence: five new baseline controls failed; three additional reader-handoff controls failed the initial adaptation and pass with the structural fix. Final 168 focused checks across 16 files passed (one Windows-only skip), plus build/static/docs checks. Actual built CLI and SDK stdio MCP probes exercised controlled denial, repeated calls, restored cleanup and recovery; baseline accumulated two descriptors, candidate retained one until recovery and completed shutdown without residue. Thirty small ordinary CLI tasks retained byte-identical full replies and Evidence IDs; no performance improvement claimed. After rebase over incoming Android/docs changes, 58 affected checks and docs/static checks passed.

Limits: authored boundary denials, not natural OS faults. Active launch is mocked; no faulted real Electron process or kill/teardown verification. The separately found process/transport retry gap is #4805 and remains next.

<!-- gh-comment-id:6234412118 --> @morluto commented on GitHub (Oct 17, 2026): Completed the remaining fuse-descriptor lifecycle repair on main in f9de41371, adapting @xu91102’s prepared dbb7db16 with co-author credit. The exact descriptor lease now survives error translation, blocked admission, retry, CLI finalization and MCP shutdown. Active preflight cannot swallow its cleanup failure or outlive final close. Active-provider shutdown cancels and joins admitted captures and gates launch. Physical read failures keep precedence; exact caller reasons and completed canonical Evidence survive close and the later reader/provider handoff. One Evidence builder preserves selected/resolved paths, architecture, digests, fuses and locations. Mixed descriptor recovery clears only confirmed owners. Evidence: five new baseline controls failed; three additional reader-handoff controls failed the initial adaptation and pass with the structural fix. Final 168 focused checks across 16 files passed (one Windows-only skip), plus build/static/docs checks. Actual built CLI and SDK stdio MCP probes exercised controlled denial, repeated calls, restored cleanup and recovery; baseline accumulated two descriptors, candidate retained one until recovery and completed shutdown without residue. Thirty small ordinary CLI tasks retained byte-identical full replies and Evidence IDs; no performance improvement claimed. After rebase over incoming Android/docs changes, 58 affected checks and docs/static checks passed. Limits: authored boundary denials, not natural OS faults. Active launch is mocked; no faulted real Electron process or kill/teardown verification. The separately found process/transport retry gap is #4805 and remains next.
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#2427
No description provided.