[GH-ISSUE #3342] [Bug] Apktool version probe stays pending after timeout or cancellation #1212

Closed
opened 2026-10-16 21:02:50 +00:00 by bsakaguchi · 0 comments
Owner

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

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

inspect-apktool-client can remain blocked after its version-probe deadline and stop grace have both expired. A supported shell launcher starts a child with inherited output pipes and waits for it. The immediate shell exits after REA requests a stop, but its child survives and keeps the CLI's output collection pending. Cancellation reproduced the same failure. This is a controlled launcher reproduction without a real Apktool or JVM dependency.

At 26.010 seconds after the fixture child started, the timeout reproduction observed a pending CLI, an exited shell, and a surviving child reparented to PID 1. Stopping the recorded child immediately released the CLI, which exited with code 1 and capability_unavailable, stating that the selected launcher did not report a usable version. In the cancellation variant, the CLI was still running 6.022 seconds after SIGINT while the shell was gone and its child remained alive. Stopping that child released the CLI with exit code 130 and a cancelled result. An independent rerun of the timeout case observed the same pending-CLI, exited-shell, surviving-child result.

Reproduction

  1. Start from a clean checkout of morluto/rea at the baseline below and prepare its built CLI. The observed runs used dependencies and a compiled runtime that were already present; the installation and build commands are clean-checkout prerequisites.
git clone https://github.com/morluto/rea.git rea-repro
cd rea-repro
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. From the built repository root, run the following script. It creates every input file inline, waits 26 seconds after the version child starts, observes the public CLI and the two recorded processes, and then stops only the fixture's remaining processes. The final result demonstrates that releasing the child's pipes releases the stalled CLI.
python3 - <<'PYTHON'
import json, os, pathlib, signal, subprocess, time
root = pathlib.Path('version-deadline-repro').resolve()
root.mkdir()
(root / 'home').mkdir()
(root / 'temporary').mkdir()
launcher = root / 'apktool'
launcher.write_text('''#!/bin/sh
printf '%s' "$$" > "$REA_REPRO_ROOT_PID_FILE"
/bin/sleep 1000 &
printf '%s' "$!" > "$REA_REPRO_CHILD_PID_FILE"
wait
''')
launcher.chmod(0o755)
rootfile = root / 'launcher.pid'
childfile = root / 'child.pid'
env = dict(os.environ, HOME=str(root / 'home'),
           TMPDIR=str(root / 'temporary'),
           REA_APKTOOL_COMMAND=str(launcher),
           REA_REPRO_ROOT_PID_FILE=str(rootfile),
           REA_REPRO_CHILD_PID_FILE=str(childfile))
cli = subprocess.Popen(['node', 'scripts/rea.mjs',
                        'inspect-apktool-client', '--json'],
                       env=env, stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE, text=True)
pids = []
try:
    started = time.monotonic()
    while not childfile.exists() and cli.poll() is None:
        if time.monotonic() - started > 10:
            raise RuntimeError('version launcher did not start')
        time.sleep(0.05)
    pids = [int(rootfile.read_text()), int(childfile.read_text())]
    time.sleep(26)
    parents = [subprocess.run(['/bin/ps', '-p', str(pid), '-o', 'ppid='],
                              capture_output=True, text=True).stdout.strip()
               for pid in pids]
    print(json.dumps({'cli_still_running': cli.poll() is None,
                      'launcher_alive': bool(parents[0]),
                      'child_alive': bool(parents[1]),
                      'child_parent_pid': int(parents[1]) if parents[1] else None},
                     indent=2))
    for pid in pids:
        try:
            os.kill(pid, signal.SIGKILL)
        except ProcessLookupError:
            pass
    out, err = cli.communicate(timeout=10)
    result = json.loads(out)
    print(json.dumps({'cli_exit_after_child_stopped': cli.returncode,
                      'result_code': result.get('code'), 'stderr': err}, indent=2))
finally:
    for pidfile in (rootfile, childfile):
        if pidfile.exists():
            try:
                os.kill(int(pidfile.read_text()), signal.SIGKILL)
            except ProcessLookupError:
                pass
    if cli.poll() is None:
        cli.kill()
        cli.communicate()
PYTHON
  1. For the cancellation variant, run the same script with cli.send_signal(signal.SIGINT) immediately after pids = [int(rootfile.read_text()), int(childfile.read_text())] and replace time.sleep(26) with time.sleep(6). Use a fresh checkout directory or remove the previous version-deadline-repro fixture directory before repeating. This cancellation variant was also executed.

Expected behavior

The version probe should stop waiting when its configured 20,000 ms deadline expires at src/apktool/ApktoolConfiguration.ts:27, with bounded cleanup of its launched processes. docs/apktool-resource-analysis.md:15 supports shell launchers. src/process/ExecFileOutput.ts:12 describes a stoppable command that waits for process exit and escalates after a grace period; src/process/ExecFileOutput.ts:22 defines that grace as 5,000 ms.

Request cancellation should likewise complete after cleanup instead of remaining dependent on a surviving wrapper child's lifetime. A launcher that never prints a version must yield an actionable unavailable-client result instead of keeping readiness inspection blocked.

Impact and scope

P2, supported. A selected wrapper child can keep readiness inspection pending after both timeout and cancellation while continuing to run after the immediate launcher exits. The same readiness probe precedes decoding, so affected launchers also block that workflow before admission completes. The executed cases used a controlled stalled launcher and do not establish frequency with real installations.

A workaround is available. Stopping the exact remaining child released the stalled CLI in both timeout and cancellation runs. This is manual recovery for a known owned process, not automatic enforcement of the configured deadline.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64. Dependencies and the compiled runtime were already present for the observed reproductions.

Target and artifact context

A controlled executable shell launcher selected through REA_APKTOOL_COMMAND, with an owned /bin/sleep 1000 child inheriting its output pipes. The script creates isolated HOME and temporary directories and records the shell and child PIDs. No APK, real Apktool, or JVM is required.

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

The timeout reproduction observed the following at 26.010 seconds after the child started:

{
  "cli_still_running": true,
  "launcher_alive": false,
  "child_alive": true,
  "child_parent_pid": 1
}

Stopping the recorded child released the CLI with exit code 1 and capability_unavailable. The cancellation run remained pending 6.022 seconds after SIGINT with the same exited-shell and surviving-child state; stopping the child released the CLI with exit code 130 and a cancelled result. The timeout result reproduced independently.

Typed outcome or recovery expected

Run the supplied timeout case and confirm that the CLI has returned its unavailable-client result by the existing deadline plus cleanup grace and that neither recorded fixture process remains alive. Execute the cancellation variant and confirm prompt cancelled completion with both owned processes gone. A control launcher that prints a valid version and exits should still report readiness successfully. Closing collection alone while leaving the child running does not restore the lifecycle contract.

Analysis or suspected cause

Source-derived reasoning: src/apktool/ApktoolConfiguration.ts:243 reaches stoppable execFileOutput. src/process/ExecFileOutput.ts:159 settles through the execFile completion callback, while the stop path at src/process/ExecFileOutput.ts:181 sends signals only through the immediate child's handle. A child retaining stdout/stderr can outlive that shell and prevent the callback from settling. This source explanation is consistent with the observed release of the CLI immediately after the recorded child was stopped.

Additional context

  • #1774 concerns Windows batch launchers failing to spawn, whereas this launcher starts and its child survives stopping.
  • #1945 concerns configuration failure mapping and remediation, whereas this probe remains pending before returning a result.
  • #3068 concerns availability inventory despite a successful readiness call, whereas this readiness call itself stays pending.
  • #1233 concerns cancellation during process-capture post-exit settlement, whereas this version probe awaits execFile completion while inherited pipes remain open.
  • #1253 requests a process-capture finalization interval, whereas this issue concerns enforcement of an existing version-probe deadline and cleanup grace.
  • #2290 concerns CLI nontermination after a reported static-analysis cleanup failure when process inspection is denied, whereas this probe has not returned its terminal result.
  • #1993 concerns Darwin compiler failure classification, whereas this issue concerns a surviving launcher child preventing completion.
  • #2549 concerns retained IDA supervisor pipes after a terminal cleanup result, whereas this issue concerns Apktool version-probe descendant cleanup before the result settles.
  • #3185 concerns stale LLDB cleanup after rejection before target launch, whereas this case has a launched surviving child.
  • #1191 concerns npx signal relay interrupting Ghidra cleanup, whereas this case uses a direct REA signal and also reproduces through timeout alone.
  • #1232 concerns a process-capture target surviving after its CLI exits, whereas this separate version-probe path keeps the CLI pending.
  • #717 sets general Android integration lifecycle requirements, whereas this issue reports a specific reproduced version-probe failure.
Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3342 ### 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 `inspect-apktool-client` can remain blocked after its version-probe deadline and stop grace have both expired. A supported shell launcher starts a child with inherited output pipes and waits for it. The immediate shell exits after REA requests a stop, but its child survives and keeps the CLI's output collection pending. Cancellation reproduced the same failure. This is a controlled launcher reproduction without a real Apktool or JVM dependency. At 26.010 seconds after the fixture child started, the timeout reproduction observed a pending CLI, an exited shell, and a surviving child reparented to PID 1. Stopping the recorded child immediately released the CLI, which exited with code 1 and `capability_unavailable`, stating that the selected launcher did not report a usable version. In the cancellation variant, the CLI was still running 6.022 seconds after SIGINT while the shell was gone and its child remained alive. Stopping that child released the CLI with exit code 130 and a cancelled result. An independent rerun of the timeout case observed the same pending-CLI, exited-shell, surviving-child result. ### Reproduction 1. Start from a clean checkout of `morluto/rea` at the baseline below and prepare its built CLI. The observed runs used dependencies and a compiled runtime that were already present; the installation and build commands are clean-checkout prerequisites. ```sh git clone https://github.com/morluto/rea.git rea-repro cd rea-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. From the built repository root, run the following script. It creates every input file inline, waits 26 seconds after the version child starts, observes the public CLI and the two recorded processes, and then stops only the fixture's remaining processes. The final result demonstrates that releasing the child's pipes releases the stalled CLI. ```sh python3 - <<'PYTHON' import json, os, pathlib, signal, subprocess, time root = pathlib.Path('version-deadline-repro').resolve() root.mkdir() (root / 'home').mkdir() (root / 'temporary').mkdir() launcher = root / 'apktool' launcher.write_text('''#!/bin/sh printf '%s' "$$" > "$REA_REPRO_ROOT_PID_FILE" /bin/sleep 1000 & printf '%s' "$!" > "$REA_REPRO_CHILD_PID_FILE" wait ''') launcher.chmod(0o755) rootfile = root / 'launcher.pid' childfile = root / 'child.pid' env = dict(os.environ, HOME=str(root / 'home'), TMPDIR=str(root / 'temporary'), REA_APKTOOL_COMMAND=str(launcher), REA_REPRO_ROOT_PID_FILE=str(rootfile), REA_REPRO_CHILD_PID_FILE=str(childfile)) cli = subprocess.Popen(['node', 'scripts/rea.mjs', 'inspect-apktool-client', '--json'], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) pids = [] try: started = time.monotonic() while not childfile.exists() and cli.poll() is None: if time.monotonic() - started > 10: raise RuntimeError('version launcher did not start') time.sleep(0.05) pids = [int(rootfile.read_text()), int(childfile.read_text())] time.sleep(26) parents = [subprocess.run(['/bin/ps', '-p', str(pid), '-o', 'ppid='], capture_output=True, text=True).stdout.strip() for pid in pids] print(json.dumps({'cli_still_running': cli.poll() is None, 'launcher_alive': bool(parents[0]), 'child_alive': bool(parents[1]), 'child_parent_pid': int(parents[1]) if parents[1] else None}, indent=2)) for pid in pids: try: os.kill(pid, signal.SIGKILL) except ProcessLookupError: pass out, err = cli.communicate(timeout=10) result = json.loads(out) print(json.dumps({'cli_exit_after_child_stopped': cli.returncode, 'result_code': result.get('code'), 'stderr': err}, indent=2)) finally: for pidfile in (rootfile, childfile): if pidfile.exists(): try: os.kill(int(pidfile.read_text()), signal.SIGKILL) except ProcessLookupError: pass if cli.poll() is None: cli.kill() cli.communicate() PYTHON ``` 3. For the cancellation variant, run the same script with `cli.send_signal(signal.SIGINT)` immediately after `pids = [int(rootfile.read_text()), int(childfile.read_text())]` and replace `time.sleep(26)` with `time.sleep(6)`. Use a fresh checkout directory or remove the previous `version-deadline-repro` fixture directory before repeating. This cancellation variant was also executed. ### Expected behavior The version probe should stop waiting when its configured 20,000 ms deadline expires at `src/apktool/ApktoolConfiguration.ts:27`, with bounded cleanup of its launched processes. `docs/apktool-resource-analysis.md:15` supports shell launchers. `src/process/ExecFileOutput.ts:12` describes a stoppable command that waits for process exit and escalates after a grace period; `src/process/ExecFileOutput.ts:22` defines that grace as 5,000 ms. Request cancellation should likewise complete after cleanup instead of remaining dependent on a surviving wrapper child's lifetime. A launcher that never prints a version must yield an actionable unavailable-client result instead of keeping readiness inspection blocked. ### Impact and scope P2, supported. A selected wrapper child can keep readiness inspection pending after both timeout and cancellation while continuing to run after the immediate launcher exits. The same readiness probe precedes decoding, so affected launchers also block that workflow before admission completes. The executed cases used a controlled stalled launcher and do not establish frequency with real installations. A workaround is available. Stopping the exact remaining child released the stalled CLI in both timeout and cancellation runs. This is manual recovery for a known owned process, not automatic enforcement of the configured deadline. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64. Dependencies and the compiled runtime were already present for the observed reproductions. ### Target and artifact context A controlled executable shell launcher selected through `REA_APKTOOL_COMMAND`, with an owned `/bin/sleep 1000` child inheriting its output pipes. The script creates isolated HOME and temporary directories and records the shell and child PIDs. No APK, real Apktool, or JVM is required. ### Interface CLI ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence The timeout reproduction observed the following at 26.010 seconds after the child started: ```text { "cli_still_running": true, "launcher_alive": false, "child_alive": true, "child_parent_pid": 1 } ``` Stopping the recorded child released the CLI with exit code 1 and `capability_unavailable`. The cancellation run remained pending 6.022 seconds after SIGINT with the same exited-shell and surviving-child state; stopping the child released the CLI with exit code 130 and a cancelled result. The timeout result reproduced independently. ### Typed outcome or recovery expected Run the supplied timeout case and confirm that the CLI has returned its unavailable-client result by the existing deadline plus cleanup grace and that neither recorded fixture process remains alive. Execute the cancellation variant and confirm prompt cancelled completion with both owned processes gone. A control launcher that prints a valid version and exits should still report readiness successfully. Closing collection alone while leaving the child running does not restore the lifecycle contract. ### Analysis or suspected cause Source-derived reasoning: `src/apktool/ApktoolConfiguration.ts:243` reaches stoppable `execFileOutput`. `src/process/ExecFileOutput.ts:159` settles through the execFile completion callback, while the stop path at `src/process/ExecFileOutput.ts:181` sends signals only through the immediate child's handle. A child retaining stdout/stderr can outlive that shell and prevent the callback from settling. This source explanation is consistent with the observed release of the CLI immediately after the recorded child was stopped. ### Additional context - [#1774](https://github.com/morluto/rea/issues/1774) concerns Windows batch launchers failing to spawn, whereas this launcher starts and its child survives stopping. - [#1945](https://github.com/morluto/rea/issues/1945) concerns configuration failure mapping and remediation, whereas this probe remains pending before returning a result. - [#3068](https://github.com/morluto/rea/issues/3068) concerns availability inventory despite a successful readiness call, whereas this readiness call itself stays pending. - [#1233](https://github.com/morluto/rea/issues/1233) concerns cancellation during process-capture post-exit settlement, whereas this version probe awaits execFile completion while inherited pipes remain open. - [#1253](https://github.com/morluto/rea/issues/1253) requests a process-capture finalization interval, whereas this issue concerns enforcement of an existing version-probe deadline and cleanup grace. - [#2290](https://github.com/morluto/rea/issues/2290) concerns CLI nontermination after a reported static-analysis cleanup failure when process inspection is denied, whereas this probe has not returned its terminal result. - [#1993](https://github.com/morluto/rea/issues/1993) concerns Darwin compiler failure classification, whereas this issue concerns a surviving launcher child preventing completion. - [#2549](https://github.com/morluto/rea/issues/2549) concerns retained IDA supervisor pipes after a terminal cleanup result, whereas this issue concerns Apktool version-probe descendant cleanup before the result settles. - [#3185](https://github.com/morluto/rea/issues/3185) concerns stale LLDB cleanup after rejection before target launch, whereas this case has a launched surviving child. - [#1191](https://github.com/morluto/rea/issues/1191) concerns npx signal relay interrupting Ghidra cleanup, whereas this case uses a direct REA signal and also reproduces through timeout alone. - [#1232](https://github.com/morluto/rea/issues/1232) concerns a process-capture target surviving after its CLI exits, whereas this separate version-probe path keeps the CLI pending. - [#717](https://github.com/morluto/rea/issues/717) sets general Android integration lifecycle requirements, whereas this issue reports a specific reproduced version-probe failure.
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#1212
No description provided.