[GH-ISSUE #5557] [Bug] Doctor waits past the Kuna version deadline when a descendant retains stdout #3385

Closed
opened 2026-10-31 00:39:48 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

Provider or capability discovery

Observed behavior

node scripts/rea.mjs doctor --provider kuna --json waits beyond its 10-second version-probe deadline and 5-second termination grace when the selected executable prints kuna 1.833, exits, and leaves a descendant holding stdout open.

An independent verifier ran the isolated public CLI reproduction below once per case at the baseline revision. Doctor returned after 23.203 seconds for a 22-second descendant, with exit 1 and version_unresolved. Redirecting only the descendant's stdout restored completion in 1.125 seconds with exit 0 and provider version 1.833. No cancellation signal was sent and no external watchdog fired.

Reproduction

From a built checkout of the baseline revision, use the repository-pinned Node.js 24.18.0 and Python 3. The verifier executed this exact snippet. All provider scripts are fake; HOME, XDG directories, cache, output and PATH are isolated. The selected fake Hopper launcher prevents real-provider discovery. Each REA invocation has a 35-second external timeout.

import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
import time

node, shell, sleep = (shutil.which(name) for name in ("node", "sh", "sleep"))
assert node and shell and sleep
root = Path(tempfile.mkdtemp(prefix="rea-version-probe-"))
for name in ("bin", "home", "config", "data", "cache", "state", "output", "temp"):
    (root / name).mkdir()
fake_hopper = root / "bin" / "fake-hopper"
fake_hopper.write_text(f"#!{shell}\nexit 0\n")
fake_hopper.chmod(0o755)
env = {
    "HOME": str(root / "home"),
    "XDG_CONFIG_HOME": str(root / "config"),
    "XDG_DATA_HOME": str(root / "data"),
    "XDG_CACHE_HOME": str(root / "cache"),
    "XDG_STATE_HOME": str(root / "state"),
    "TMPDIR": str(root / "temp"),
    "PATH": str(root / "bin"),
    "HOPPER_LAUNCHER_PATH": str(fake_hopper),
    "REA_KUNA_CACHE_DIR": str(root / "cache" / "kuna"),
    "REA_LOG_LEVEL": "silent",
}
for case in ("plain", "held", "redirected", "blocking"):
    fake = root / "bin" / ("kuna-" + case)
    pidfile = root / (case + ".pid")
    body = f"#!{shell}\nprintf 'kuna 1.833\\n'\n"
    if case in ("held", "redirected"):
        redirect = " > /dev/null" if case == "redirected" else ""
        body += f"{shlex.quote(sleep)} 22{redirect} 2>/dev/null &\n"
        body += f"printf '%s\\n' \"$!\" > {shlex.quote(str(pidfile))}\n"
    elif case == "blocking":
        body += f"exec {shlex.quote(sleep)} 22\n"
    body += "exit 0\n"
    fake.write_text(body)
    fake.chmod(0o755)
    started = time.monotonic()
    try:
        with (root / "output" / (case + ".json")).open("w") as out:
            with (root / "output" / (case + ".stderr")).open("w") as err:
                result = subprocess.run(
                    [node, "scripts/rea.mjs", "doctor", "--provider", "kuna", "--json"],
                    env={**env, "REA_KUNA_COMMAND": str(fake)},
                    stdout=out, stderr=err, timeout=35,
                )
        elapsed = time.monotonic() - started
        report = json.loads((root / "output" / (case + ".json")).read_text())
        kuna = next(p for p in report["providerInspections"] if p["id"] == "kuna")
        print(case, round(elapsed, 3), result.returncode, kuna["available"],
              kuna["providerVersion"], kuna["checks"][0]["code"])
    finally:
        if pidfile.exists():
            try:
                os.kill(int(pidfile.read_text()), 15)
            except ProcessLookupError:
                pass

Expected behavior

Doctor should settle within the version deadline plus bounded termination/output-drain grace, even after the direct process exits while a descendant holds its output pipe open. A truthful structured failure is acceptable when output completeness or cleanup cannot be established; this report does not require accepting incomplete version output.

Source-derived contract at the baseline: src/kuna/KunaInstallation.ts:35-36,173-181 sets the 10-second deadline and escalation to prevent doctor hangs. tests/boundary/providers/kuna/kunaInstallation.test.ts:199-221 explicitly requires deadline-plus-bounded-grace settlement. docs/tool-design.md:73-88 distinguishes process exit from output closure and describes bounded draining.

Impact and scope

P2. A local provider readiness check can remain blocked beyond its intended deadline and grace for this caller-selected executable behavior. This was demonstrated with fake scripts, not a real Kuna distribution. No analysis database or target binary was involved, and no security boundary crossing or evidence loss was demonstrated. Frequency with real Kuna releases or wrappers is unknown.

The stdout-redirection control demonstrates a workaround for caller-controlled wrappers. Applicability to an affected real distribution is untested. An indefinitely held pipe was not tested.

REA version

6.7.0, source baseline 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node.js 24.18.0; macOS arm64. Fake provider scripts only; no real Kuna or Hopper command was executed.

Target and artifact context

No analysis target. Caller-selected executable shell fixtures print kuna 1.833; the failing fixture starts a 22-second background sleep with inherited stdout and redirected stderr, then exits. The control redirects that descendant's stdout too.

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Independent verifier's exact-snippet baseline replay, one sample per row:

Fake executable Elapsed seconds CLI exit Kuna available Provider version Installation code
Prints version and exits 1.487 0 true 1.833 null
Exits, descendant retains stdout 23.203 1 false null version_unresolved
Same descendant, stdout redirected 1.125 0 true 1.833 null
Prints version, then directly blocks via exec sleep 11.057 1 false null version_unresolved

The retained-stdout result reported Command timed out after 10000 ms, deadline_ms: 10000, killed: true, and signal: null. The verifier also observed that the direct shell was already absent shortly after spawning its descendant. A separate 12-second descendant run took 13.167 seconds. These finite observations show settlement following descendant pipe lifetime.

Submission recheck on freshly built current main d2a3269d0c1007e16a2f84d44cc0b1503debb2ae, Node.js 24.18.0 on macOS arm64, used the same snippet. Results were plain 1.309 seconds / exit 0 / version 1.833; held stdout 23.167 seconds / exit 1 / version_unresolved; redirected stdout 1.157 seconds / exit 0 / version 1.833; directly blocking 10.992 seconds / exit 1 / version_unresolved. All four completed without external timeout. The sibling cancellation fix d532b18123916a9e88f98c7880ba4f984e23d715 is included in that revision.

Typed outcome or recovery expected

Return a terminal doctor JSON result within the bounded probe lifecycle, with truthful availability, failure and output-completeness facts. Preserve the successful plain fixture and the ordinary timeout for a directly blocking command. Any process cleanup must retain verified ownership and avoid signalling unrelated processes.

Analysis or suspected cause

Source-derived reasoning: the baseline public CLI reaches src/cli/setupCommands.ts:106-141, src/doctorRuntime.ts:53-70, src/kuna/KunaDoctor.ts:39-50, and src/kuna/KunaInstallation.ts:173-199. The version probe calls execFileOutput with stopSignal: "SIGTERM".

src/process/ExecFileOutput.ts:140-199 settles through the captured-output callback. Its deadline and five-second escalation signal only the direct child. After that child exits, a descendant retaining stdout can keep the callback pending. The passing stdout-redirection control supports this explanation.

Additional context

Prepared with AI assistance and independently reproduced. The submission check also replayed the same fixture against freshly built current main, as recorded above. docs/kuna-provider.md:7-14 documents the selected executable and doctor entry point.

Related but distinct: #5106 and merged #5241 concern caller cancellation propagation; this reproduction sends no cancellation. #5554 concerns a dangling cache-root symlink. #3342 and merged #3808 concern the analogous Apktool probe, which was moved to an owned-command implementation; this report concerns the still-reachable Kuna probe.

NOT_ENOUGH_INFO: the last known good revision and prevalence with real Kuna releases or wrappers have not been established.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5557 ### 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 Provider or capability discovery ### Observed behavior `node scripts/rea.mjs doctor --provider kuna --json` waits beyond its 10-second version-probe deadline and 5-second termination grace when the selected executable prints `kuna 1.833`, exits, and leaves a descendant holding stdout open. An independent verifier ran the isolated public CLI reproduction below once per case at the baseline revision. Doctor returned after 23.203 seconds for a 22-second descendant, with exit 1 and `version_unresolved`. Redirecting only the descendant's stdout restored completion in 1.125 seconds with exit 0 and provider version `1.833`. No cancellation signal was sent and no external watchdog fired. ### Reproduction From a built checkout of the baseline revision, use the repository-pinned Node.js 24.18.0 and Python 3. The verifier executed this exact snippet. All provider scripts are fake; HOME, XDG directories, cache, output and PATH are isolated. The selected fake Hopper launcher prevents real-provider discovery. Each REA invocation has a 35-second external timeout. ```python import json import os from pathlib import Path import shlex import shutil import subprocess import tempfile import time node, shell, sleep = (shutil.which(name) for name in ("node", "sh", "sleep")) assert node and shell and sleep root = Path(tempfile.mkdtemp(prefix="rea-version-probe-")) for name in ("bin", "home", "config", "data", "cache", "state", "output", "temp"): (root / name).mkdir() fake_hopper = root / "bin" / "fake-hopper" fake_hopper.write_text(f"#!{shell}\nexit 0\n") fake_hopper.chmod(0o755) env = { "HOME": str(root / "home"), "XDG_CONFIG_HOME": str(root / "config"), "XDG_DATA_HOME": str(root / "data"), "XDG_CACHE_HOME": str(root / "cache"), "XDG_STATE_HOME": str(root / "state"), "TMPDIR": str(root / "temp"), "PATH": str(root / "bin"), "HOPPER_LAUNCHER_PATH": str(fake_hopper), "REA_KUNA_CACHE_DIR": str(root / "cache" / "kuna"), "REA_LOG_LEVEL": "silent", } for case in ("plain", "held", "redirected", "blocking"): fake = root / "bin" / ("kuna-" + case) pidfile = root / (case + ".pid") body = f"#!{shell}\nprintf 'kuna 1.833\\n'\n" if case in ("held", "redirected"): redirect = " > /dev/null" if case == "redirected" else "" body += f"{shlex.quote(sleep)} 22{redirect} 2>/dev/null &\n" body += f"printf '%s\\n' \"$!\" > {shlex.quote(str(pidfile))}\n" elif case == "blocking": body += f"exec {shlex.quote(sleep)} 22\n" body += "exit 0\n" fake.write_text(body) fake.chmod(0o755) started = time.monotonic() try: with (root / "output" / (case + ".json")).open("w") as out: with (root / "output" / (case + ".stderr")).open("w") as err: result = subprocess.run( [node, "scripts/rea.mjs", "doctor", "--provider", "kuna", "--json"], env={**env, "REA_KUNA_COMMAND": str(fake)}, stdout=out, stderr=err, timeout=35, ) elapsed = time.monotonic() - started report = json.loads((root / "output" / (case + ".json")).read_text()) kuna = next(p for p in report["providerInspections"] if p["id"] == "kuna") print(case, round(elapsed, 3), result.returncode, kuna["available"], kuna["providerVersion"], kuna["checks"][0]["code"]) finally: if pidfile.exists(): try: os.kill(int(pidfile.read_text()), 15) except ProcessLookupError: pass ``` ### Expected behavior Doctor should settle within the version deadline plus bounded termination/output-drain grace, even after the direct process exits while a descendant holds its output pipe open. A truthful structured failure is acceptable when output completeness or cleanup cannot be established; this report does not require accepting incomplete version output. Source-derived contract at the baseline: `src/kuna/KunaInstallation.ts:35-36,173-181` sets the 10-second deadline and escalation to prevent doctor hangs. `tests/boundary/providers/kuna/kunaInstallation.test.ts:199-221` explicitly requires deadline-plus-bounded-grace settlement. `docs/tool-design.md:73-88` distinguishes process exit from output closure and describes bounded draining. ### Impact and scope P2. A local provider readiness check can remain blocked beyond its intended deadline and grace for this caller-selected executable behavior. This was demonstrated with fake scripts, not a real Kuna distribution. No analysis database or target binary was involved, and no security boundary crossing or evidence loss was demonstrated. Frequency with real Kuna releases or wrappers is unknown. The stdout-redirection control demonstrates a workaround for caller-controlled wrappers. Applicability to an affected real distribution is untested. An indefinitely held pipe was not tested. ### REA version 6.7.0, source baseline `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node.js 24.18.0; macOS arm64. Fake provider scripts only; no real Kuna or Hopper command was executed. ### Target and artifact context No analysis target. Caller-selected executable shell fixtures print `kuna 1.833`; the failing fixture starts a 22-second background sleep with inherited stdout and redirected stderr, then exits. The control redirects that descendant's stdout too. ### Interface CLI ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Independent verifier's exact-snippet baseline replay, one sample per row: | Fake executable | Elapsed seconds | CLI exit | Kuna available | Provider version | Installation code | | --- | ---: | ---: | --- | --- | --- | | Prints version and exits | 1.487 | 0 | true | 1.833 | null | | Exits, descendant retains stdout | 23.203 | 1 | false | null | version_unresolved | | Same descendant, stdout redirected | 1.125 | 0 | true | 1.833 | null | | Prints version, then directly blocks via exec sleep | 11.057 | 1 | false | null | version_unresolved | The retained-stdout result reported `Command timed out after 10000 ms`, `deadline_ms: 10000`, `killed: true`, and `signal: null`. The verifier also observed that the direct shell was already absent shortly after spawning its descendant. A separate 12-second descendant run took 13.167 seconds. These finite observations show settlement following descendant pipe lifetime. Submission recheck on freshly built current main `d2a3269d0c1007e16a2f84d44cc0b1503debb2ae`, Node.js 24.18.0 on macOS arm64, used the same snippet. Results were plain 1.309 seconds / exit 0 / version `1.833`; held stdout 23.167 seconds / exit 1 / `version_unresolved`; redirected stdout 1.157 seconds / exit 0 / version `1.833`; directly blocking 10.992 seconds / exit 1 / `version_unresolved`. All four completed without external timeout. The sibling cancellation fix `d532b18123916a9e88f98c7880ba4f984e23d715` is included in that revision. ### Typed outcome or recovery expected Return a terminal doctor JSON result within the bounded probe lifecycle, with truthful availability, failure and output-completeness facts. Preserve the successful plain fixture and the ordinary timeout for a directly blocking command. Any process cleanup must retain verified ownership and avoid signalling unrelated processes. ### Analysis or suspected cause Source-derived reasoning: the baseline public CLI reaches `src/cli/setupCommands.ts:106-141`, `src/doctorRuntime.ts:53-70`, `src/kuna/KunaDoctor.ts:39-50`, and `src/kuna/KunaInstallation.ts:173-199`. The version probe calls `execFileOutput` with `stopSignal: "SIGTERM"`. `src/process/ExecFileOutput.ts:140-199` settles through the captured-output callback. Its deadline and five-second escalation signal only the direct child. After that child exits, a descendant retaining stdout can keep the callback pending. The passing stdout-redirection control supports this explanation. ### Additional context Prepared with AI assistance and independently reproduced. The submission check also replayed the same fixture against freshly built current main, as recorded above. `docs/kuna-provider.md:7-14` documents the selected executable and doctor entry point. Related but distinct: #5106 and merged #5241 concern caller cancellation propagation; this reproduction sends no cancellation. #5554 concerns a dangling cache-root symlink. #3342 and merged #3808 concern the analogous Apktool probe, which was moved to an owned-command implementation; this report concerns the still-reachable Kuna probe. NOT_ENOUGH_INFO: the last known good revision and prevalence with real Kuna releases or wrappers have not been established.
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#3385
No description provided.