Repository navigation
Description
Interpreter discovery can hang after a candidate times out if that candidate left a child process holding stdout or stderr open.
Affected code
src/python_discovery/_cached_py_info.py:256 at the reported commit. The reproducer also returns BUG on current main 004294957ed227fe7e52b2864952ba09fd28b220.
Mechanism
The parent probe times out, but the pipe read waits for the candidate's surviving child to close its inherited descriptors.
Reproduction
Check out the reported commit or current main. Save the files below as named, then build and run:
docker build -t python-discovery-audit -f - . <<'DOCKERFILE'
FROM python:3.15-bookworm
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY . /src
WORKDIR /src
ENV SETUPTOOLS_SCM_PRETEND_VERSION=1.9.1
RUN pip install -e . --group test
DOCKERFILE
docker run --rm --network=none --memory=2g -u 0 -w /src \
-v "$PWD/repro:/tmp/repro:ro" -v "$PWD/repro.d:/tmp/repro.d:ro" \
python-discovery-audit /tmp/reprorepro
#!/usr/bin/env python3
import contextlib
import os
import shutil
import signal
import subprocess
import sys
import tempfile
# set up: a PATH directory whose only entry is the launcher
path_dir = tempfile.mkdtemp()
launcher = shutil.copy("/tmp/repro.d/python3.11", path_dir)
os.chmod(launcher, 0o755)
# trigger: PY_DISCOVERY_TIMEOUT=1, so the probe (one attempt, one retry) should give up on python3.11 after about 2 s
caller = subprocess.Popen(
[sys.executable, "/tmp/repro.d/find_python.py", path_dir, "1"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
start_new_session=True, # the session holds the caller and everything the probe starts
)
try:
output, _ = caller.communicate(timeout=10)
hung = False
except subprocess.TimeoutExpired:
hung = True
print("caller still running after 10 s; its session:")
print(
subprocess.run(
["ps", "-o", "pid,ppid,stat,etime,args", "--sid", str(caller.pid)], capture_output=True, text=True
).stdout
)
caller.terminate() # find_python.py prints its stack on SIGTERM
output, _ = caller.communicate()
with contextlib.suppress(ProcessLookupError):
os.killpg(caller.pid, signal.SIGKILL) # the launcher's sleep survives either way
# check: a hang, at the communicate() that follows process.kill() (_cached_py_info.py:256)
print(output)
print("BUG" if hung and "line 256 in _run_subprocess" in output else "OK")repro.d/find_python.py
import faulthandler
import logging
import signal
import sys
import tempfile
import time
from pathlib import Path
from python_discovery import DiskCache, get_interpreter
path_dir, probe_timeout = sys.argv[1:]
faulthandler.register(signal.SIGTERM, chain=True) # diagnostic: on SIGTERM, print where this process is, then exit
logging.basicConfig(level=logging.INFO)
logging.getLogger("python_discovery").setLevel(logging.DEBUG) # logs every command the library spawns
started = time.monotonic()
found = get_interpreter(
"python3.11",
cache=DiskCache(Path(tempfile.mkdtemp())),
env={"PATH": path_dir, "HOME": "/nonexistent", "PY_DISCOVERY_TIMEOUT": probe_timeout},
)
print(f"get_interpreter returned after {time.monotonic() - started:.1f}s: {found}")repro.d/python3.11
#!/bin/sh
# A launcher for Python 3.11 that does not `exec`: each command runs as a child of this shell.
# The sleep stands in for a start-up step that is stuck (hung network mount, licence server, ...).
/bin/sleep 3600
/usr/bin/python3.11 "$@"Output on 004294957ed227fe7e52b2864952ba09fd28b220:
caller still running after 10 s; its session:
PID PPID STAT ELAPSED COMMAND
7 1 Ss 00:10 /opt/venv/bin/python3 /tmp/repro.d/find_python.py /tmp/tmp2cg3mkf3 1
9 7 Z 00:09 [python3.11] <defunct>
10 1 S 00:09 /bin/sleep 3600
INFO:python_discovery._discovery:find interpreter for spec PythonSpec(major=3, minor=11, free_threaded=False)
DEBUG:python_discovery._cached_py_info:get interpreter info via cmd: /usr/local/bin/python3.15 /src/src/python_discovery/_py_info_collect.py 57e5b384972dc3646056bf5411cfee6d df70c167e993af81581e1bbfcf8ad199
DEBUG:python_discovery._cache:wrote python info at /tmp/tmp4qe0xqg8/py_info/4/755e5f8e9348854516c9513236b846adb15e9a21490fd9dc391537946af27457.json
DEBUG:python_discovery._compat:filesystem is case-sensitive
INFO:python_discovery._discovery:proposed PythonInfo(spec=CPython3.15.2.final.0-64-x86_64, system=/usr/local/bin/python3.15, exe=/opt/venv/bin/python3, platform=linux, version='3.15.2 (main, Oct 28 2026, 04:34:17) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
DEBUG:python_discovery._discovery:discover PATH[0]=/tmp/tmp2cg3mkf3
DEBUG:python_discovery._cached_py_info:get interpreter info via cmd: /tmp/tmp2cg3mkf3/python3.11 /src/src/python_discovery/_py_info_collect.py 27f4d764ee6281b282bc926a23797e85 595e55f77f9b1cc309f6fc54928c15bb
Current thread 0x00007fd5c2824b80 [python3] (most recent call first):
File "/usr/local/lib/python3.15/selectors.py", line 398 in select
File "/usr/local/lib/python3.15/subprocess.py", line 291 in _communicate_io_posix
File "/usr/local/lib/python3.15/subprocess.py", line 2412 in _communicate
File "/usr/local/lib/python3.15/subprocess.py", line 1395 in communicate
File "/src/src/python_discovery/_cached_py_info.py", line 256 in _run_subprocess
File "/src/src/python_discovery/_cached_py_info.py", line 127 in _get_via_file_cache
File "/src/src/python_discovery/_cached_py_info.py", line 87 in _get_from_cache
File "/src/src/python_discovery/_cached_py_info.py", line 65 in from_exe
File "/src/src/python_discovery/_py_info.py", line 411 in _from_exe_with_args
File "/src/src/python_discovery/_py_info.py", line 385 in from_exe
File "/src/src/python_discovery/_discovery.py", line 261 in _propose_from_path
File "/src/src/python_discovery/_discovery.py", line 203 in propose_interpreters
File "/src/src/python_discovery/_discovery.py", line 135 in _find_interpreter
File "/src/src/python_discovery/_discovery.py", line 52 in get_interpreter
File "/tmp/repro.d/find_python.py", line 17 in <module>
BUG
Expected: The query should return within its timeout when a child retains the pipes.
Impact
Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is niche: the candidate must launch a child that keeps its inherited output handles open. The reproduced effect stays within the caller's configured cache, search path, or interpreter.
Credits
Reported by Anthropic's OSS Scanner: ANT-2026-RASICDKU.
Activity