[GH-ISSUE #3887] [Bug] Named-pipe firmware input blocks cancellation and the operation deadline #1508

Closed
opened 2026-10-16 21:06:20 +00:00 by bsakaguchi · 3 comments
Owner

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

snapshotFirmware awaits a blocking read-only open() before checking the file type, so a caller-selected FIFO with no writer never returns: the pending open ignores the abort signal, and the compiled reader stays pending after cancellation and requires forced termination.

snapshotFirmware awaits a blocking open(source, O_RDONLY | O_NOFOLLOW) at src/firmware/FirmwareFiles.ts:68 before the regular-file check at line 73, and opening a FIFO without a writer never reaches that check.

The pending open does not consume the abort signal: the executed compiled reader remained pending after cancellation twice and required forced termination, while the regular-file control fulfilled with size: 8.

On the public Linux path the same pending acquisition precedes producer version checking and prevents the operation from returning when its deadline or cancellation signal fires; later requests on the same provider stay queued and close() awaits that queue at src/firmware/FirmwareProvider.ts:153.

Reproduction

The bounded reader probe below was executed against the compiled runtime from a built source checkout, with an eight-byte regular-file control and a FIFO left without a writer. It kills only its own blocked probe children.

checkout="$PWD"
work="$(mktemp -d)"
printf 'firmware' > "$work/input.bin"
mkfifo "$work/input.fifo"
cat > "$work/probe-reader.mjs" <<'JS'
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const { snapshotFirmware } = await import(pathToFileURL(
  join(process.argv[2], 'dist/firmware/FirmwareFiles.js')));
const signal = AbortSignal.timeout(50);
let state = 'pending';
void snapshotFirmware({
  operation: 'inspect_firmware_regions',
  input: { path: process.argv[3] }
}, process.argv[4], signal).then(
  value => { state = `fulfilled:${value.size}`; },
  error => { state = error.constructor.name; });
await new Promise(resolve => setTimeout(resolve, 250));
console.log(JSON.stringify({
  signal_aborted: signal.aborted, snapshot_state: state
}));
JS
python3 - "$checkout" "$work" <<'PY'
from pathlib import Path
import subprocess
import sys
checkout, work = sys.argv[1], Path(sys.argv[2])
for label, source in [('regular', 'input.bin'),
                      ('fifo-first', 'input.fifo'),
                      ('fifo-repeat', 'input.fifo')]:
    target = work / label
    target.mkdir()
    child = subprocess.Popen([
        'nice', '-n', '10', 'node', str(work / 'probe-reader.mjs'),
        checkout, str(work / source), str(target)
    ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    forced = False
    try:
        output, errors = child.communicate(timeout=0.8)
    except subprocess.TimeoutExpired:
        forced = True
        child.kill()
        output, errors = child.communicate(timeout=2)
    print(label, 'forced_stop=' + str(forced))
    print(output, end='')
    if errors:
        print(errors, end='')
PY

Expected behavior

docs/firmware-analysis.md:155 restricts input to nonempty regular files, docs/firmware-analysis.md:159 specifies a 120-second deadline, and src/firmware/FirmwareFiles.ts:73 explicitly intends to reject nonregular input.

The CLI translates SIGINT/SIGTERM to the operation's abort signal at src/cli/commandCancellation.ts:15, so nonregular input should produce a typed rejection without blocking cancellation.

A caller-selected FIFO should not be able to block both firmware operations before an engine starts, and cooperative cancellation or provider shutdown should still settle the acquisition.

Impact and scope

a caller-selected local FIFO without a writer blocks both firmware operations before an engine starts, cooperative cancellation and provider shutdown cannot settle the acquisition, and a separate process may require forced termination; the trigger is a bounded invalid local input.

Workaround: available. regular-file input avoids the hang, as the control verified; no option was found that lets cancellation settle an already-selected FIFO.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64; The end-to-end public CLI/MCP path on Linux (producer version checking, the 120-second deadline elapse) was not executed; the host is macOS and the required producer tools are absent. The compiled production reader boundary was executed and observed directly.

Target and artifact context

Firmware image file / named-pipe (FIFO) input

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

  • Expected source: docs/firmware-analysis.md:155, docs/firmware-analysis.md:159; nonregular-input rejection intent at src/firmware/FirmwareFiles.ts:73; cancellation translation at src/cli/commandCancellation.ts:15
  • Failure source: observed reader remaining pending after abort on both FIFO runs (output below); static cause at src/firmware/FirmwareFiles.ts:68 (blocking open without nonblocking mode or an abortable admission step, before the type check at line 73); the shared reader at src/filesystem/RegularFile.ts:67 already uses nonblocking admission but firmware does not
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: incomplete
  • Missing fact: the full public Linux CLI/MCP run with a real producer and an elapsed 120-second deadline was not executed (host is macOS; producer tools absent); public reachability through src/firmware/FirmwareProvider.ts:230-245 is source-derived.
regular forced_stop=False
{"signal_aborted":true,"snapshot_state":"fulfilled:8"}
fifo-first forced_stop=True
{"signal_aborted":true,"snapshot_state":"pending"}
fifo-repeat forced_stop=True
{"signal_aborted":true,"snapshot_state":"pending"}

Typed outcome or recovery expected

A nonregular (FIFO) input should produce a typed nonregular-input rejection that observes the abort signal and the 120-second deadline, instead of a never-returning blocked open that requires forced termination.

Restoration check:

Failing: with a FIFO that has no writer, snapshotFirmware stays pending after its abort signal fires and the probe child requires forced termination on both runs.

Passing: the same FIFO input rejects promptly with a typed nonregular-input error that observes the abort signal, while the regular-file control still fulfills with size: 8.

Analysis or suspected cause

snapshotFirmware (FirmwareFiles.ts:68) awaits a blocking open(O_RDONLY|O_NOFOLLOW) before the regular-file type check at line 73, so opening a writer-less FIFO blocks and ignores the abort signal; the signal only reaches the later pipeline at FirmwareProvider.ts:101/245 after the open, the queue slot releases only at line 145, and close() waits on it at line 153. RegularFile.ts:67 already uses nonblocking admission; firmware does not.

Additional context

Public reachability is source-only: src/cli/firmwareCommands.ts:47 resolves a local path, src/domain/firmware/firmwareAnalysis.ts:9 checks only string/absolute shape, src/application/firmware/FirmwareAnalysisService.ts:33-36 dispatches, and src/firmware/FirmwareProvider.ts:230 creates the 120-second deadline and awaits snapshotFirmware at line 245; the signal reaches the later pipeline at line 101 only after the blocked open. This affects inspect-firmware-regions, extract-firmware, and the corresponding MCP tools.

Related issues: closed #3332 fixes the same failure class in JavaScript recovery, a separate reader; closed #3218 concerns historical-source FIFO classification. The firmware reader still contains the blocking-open pattern. No exact firmware duplicate found.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3887 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior `snapshotFirmware` awaits a blocking read-only `open()` before checking the file type, so a caller-selected FIFO with no writer never returns: the pending open ignores the abort signal, and the compiled reader stays pending after cancellation and requires forced termination. `snapshotFirmware` awaits a blocking `open(source, O_RDONLY | O_NOFOLLOW)` at src/firmware/FirmwareFiles.ts:68 before the regular-file check at line 73, and opening a FIFO without a writer never reaches that check. The pending open does not consume the abort signal: the executed compiled reader remained pending after cancellation twice and required forced termination, while the regular-file control fulfilled with `size: 8`. On the public Linux path the same pending acquisition precedes producer version checking and prevents the operation from returning when its deadline or cancellation signal fires; later requests on the same provider stay queued and `close()` awaits that queue at src/firmware/FirmwareProvider.ts:153. ### Reproduction The bounded reader probe below was executed against the compiled runtime from a built source checkout, with an eight-byte regular-file control and a FIFO left without a writer. It kills only its own blocked probe children. ```sh checkout="$PWD" work="$(mktemp -d)" printf 'firmware' > "$work/input.bin" mkfifo "$work/input.fifo" cat > "$work/probe-reader.mjs" <<'JS' import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; const { snapshotFirmware } = await import(pathToFileURL( join(process.argv[2], 'dist/firmware/FirmwareFiles.js'))); const signal = AbortSignal.timeout(50); let state = 'pending'; void snapshotFirmware({ operation: 'inspect_firmware_regions', input: { path: process.argv[3] } }, process.argv[4], signal).then( value => { state = `fulfilled:${value.size}`; }, error => { state = error.constructor.name; }); await new Promise(resolve => setTimeout(resolve, 250)); console.log(JSON.stringify({ signal_aborted: signal.aborted, snapshot_state: state })); JS python3 - "$checkout" "$work" <<'PY' from pathlib import Path import subprocess import sys checkout, work = sys.argv[1], Path(sys.argv[2]) for label, source in [('regular', 'input.bin'), ('fifo-first', 'input.fifo'), ('fifo-repeat', 'input.fifo')]: target = work / label target.mkdir() child = subprocess.Popen([ 'nice', '-n', '10', 'node', str(work / 'probe-reader.mjs'), checkout, str(work / source), str(target) ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) forced = False try: output, errors = child.communicate(timeout=0.8) except subprocess.TimeoutExpired: forced = True child.kill() output, errors = child.communicate(timeout=2) print(label, 'forced_stop=' + str(forced)) print(output, end='') if errors: print(errors, end='') PY ``` ### Expected behavior docs/firmware-analysis.md:155 restricts input to nonempty regular files, docs/firmware-analysis.md:159 specifies a 120-second deadline, and src/firmware/FirmwareFiles.ts:73 explicitly intends to reject nonregular input. The CLI translates SIGINT/SIGTERM to the operation's abort signal at src/cli/commandCancellation.ts:15, so nonregular input should produce a typed rejection without blocking cancellation. A caller-selected FIFO should not be able to block both firmware operations before an engine starts, and cooperative cancellation or provider shutdown should still settle the acquisition. ### Impact and scope a caller-selected local FIFO without a writer blocks both firmware operations before an engine starts, cooperative cancellation and provider shutdown cannot settle the acquisition, and a separate process may require forced termination; the trigger is a bounded invalid local input. Workaround: available. regular-file input avoids the hang, as the control verified; no option was found that lets cancellation settle an already-selected FIFO. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64; The end-to-end public CLI/MCP path on Linux (producer version checking, the 120-second deadline elapse) was not executed; the host is macOS and the required producer tools are absent. The compiled production reader boundary was executed and observed directly. ### Target and artifact context Firmware image file / named-pipe (FIFO) input ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence - Expected source: docs/firmware-analysis.md:155, docs/firmware-analysis.md:159; nonregular-input rejection intent at src/firmware/FirmwareFiles.ts:73; cancellation translation at src/cli/commandCancellation.ts:15 - Failure source: observed reader remaining `pending` after abort on both FIFO runs (output below); static cause at src/firmware/FirmwareFiles.ts:68 (blocking `open` without nonblocking mode or an abortable admission step, before the type check at line 73); the shared reader at src/filesystem/RegularFile.ts:67 already uses nonblocking admission but firmware does not - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: incomplete - Missing fact: the full public Linux CLI/MCP run with a real producer and an elapsed 120-second deadline was not executed (host is macOS; producer tools absent); public reachability through src/firmware/FirmwareProvider.ts:230-245 is source-derived. ```text regular forced_stop=False {"signal_aborted":true,"snapshot_state":"fulfilled:8"} fifo-first forced_stop=True {"signal_aborted":true,"snapshot_state":"pending"} fifo-repeat forced_stop=True {"signal_aborted":true,"snapshot_state":"pending"} ``` ### Typed outcome or recovery expected A nonregular (FIFO) input should produce a typed nonregular-input rejection that observes the abort signal and the 120-second deadline, instead of a never-returning blocked open that requires forced termination. Restoration check: Failing: with a FIFO that has no writer, `snapshotFirmware` stays `pending` after its abort signal fires and the probe child requires forced termination on both runs. Passing: the same FIFO input rejects promptly with a typed nonregular-input error that observes the abort signal, while the regular-file control still fulfills with `size: 8`. ### Analysis or suspected cause snapshotFirmware (FirmwareFiles.ts:68) awaits a blocking open(O_RDONLY|O_NOFOLLOW) before the regular-file type check at line 73, so opening a writer-less FIFO blocks and ignores the abort signal; the signal only reaches the later pipeline at FirmwareProvider.ts:101/245 after the open, the queue slot releases only at line 145, and close() waits on it at line 153. RegularFile.ts:67 already uses nonblocking admission; firmware does not. ### Additional context Public reachability is source-only: src/cli/firmwareCommands.ts:47 resolves a local path, src/domain/firmware/firmwareAnalysis.ts:9 checks only string/absolute shape, src/application/firmware/FirmwareAnalysisService.ts:33-36 dispatches, and src/firmware/FirmwareProvider.ts:230 creates the 120-second deadline and awaits `snapshotFirmware` at line 245; the signal reaches the later pipeline at line 101 only after the blocked open. This affects `inspect-firmware-regions`, `extract-firmware`, and the corresponding MCP tools. Related issues: closed #3332 fixes the same failure class in JavaScript recovery, a separate reader; closed #3218 concerns historical-source FIFO classification. The firmware reader still contains the blocking-open pattern. No exact firmware duplicate found. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Real problem: snapshotFirmware does a blocking open() on the caller-chosen path before checking it is a regular file, so a FIFO with no writer hangs forever and neither the abort signal nor the operation deadline can interrupt it. Cancellation that requires forced termination defeats the point of an abort signal, and the queued provider close() makes one stuck request poison later ones too. The fix should open with O_NONBLOCK first (or lstat the path) to reject FIFOs/sockets before the blocking read, and the pending read must be wired to the abort signal with a proper cleanup path — ideally covering the FIFO-without-writer case in the reader probe tests.

<!-- gh-comment-id:6185684675 --> @XinwangTao commented on GitHub (Oct 15, 2026): Real problem: snapshotFirmware does a blocking open() on the caller-chosen path before checking it is a regular file, so a FIFO with no writer hangs forever and neither the abort signal nor the operation deadline can interrupt it. Cancellation that requires forced termination defeats the point of an abort signal, and the queued provider close() makes one stuck request poison later ones too. The fix should open with O_NONBLOCK first (or lstat the path) to reject FIFOs/sockets before the blocking read, and the pending read must be wired to the abort signal with a proper cleanup path — ideally covering the FIFO-without-writer case in the reader probe tests.
Author
Owner

@yankikucuk commented on GitHub (Oct 15, 2026):

Working on this. Reproduced with your reader probe against the built code: the FIFO runs stayed pending after the signal fired and needed forced termination. snapshotFirmware now admits the resolved path through the shared nonblocking openRegularFile and maps its non-regular rejection to the existing typed input error, so both FIFO runs settle at once with AnalysisInputError and the regular control still returns fulfilled:8. Draft PR follows shortly.

<!-- gh-comment-id:6191445370 --> @yankikucuk commented on GitHub (Oct 15, 2026): Working on this. Reproduced with your reader probe against the built code: the FIFO runs stayed pending after the signal fired and needed forced termination. `snapshotFirmware` now admits the resolved path through the shared nonblocking `openRegularFile` and maps its non-regular rejection to the existing typed input error, so both FIFO runs settle at once with `AnalysisInputError` and the regular control still returns `fulfilled:8`. Draft PR follows shortly.
Author
Owner

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

Addressed on main by github.com/morluto/rea@2d163c12b1. Firmware snapshot admission now uses the shared nonblocking regular-file reader, rejects FIFO inputs with the typed input error, and retains exact descriptor cleanup ownership. A regression test covers a writerless FIFO through the compiled snapshot/hash/report readers.

I checked the current source and relevant regression coverage; I have not rerun the original runtime reproduction in this review. Closing as completed.

<!-- gh-comment-id:6244128156 --> @coygeek commented on GitHub (Oct 18, 2026): Addressed on main by https://github.com/morluto/rea/commit/2d163c12b18c4eb94dfb3b58d61e6e8821a71093. Firmware snapshot admission now uses the shared nonblocking regular-file reader, rejects FIFO inputs with the typed input error, and retains exact descriptor cleanup ownership. A regression test covers a writerless FIFO through the compiled snapshot/hash/report readers. I checked the current source and relevant regression coverage; I have not rerun the original runtime reproduction in this review. Closing as completed.
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#1508
No description provided.