Repository navigation
Description
A literal executable-name request can miss a symlink on PATH and fall through to another interpreter. A copy of the same executable under the same name is found.
Affected code
src/python_discovery/_py_info.py:283 at the reported commit. The reproducer also returns BUG on current main 004294957ed227fe7e52b2864952ba09fd28b220.
Mechanism
The literal-name match checks the resolved executable name, losing the symlink name that matched the request.
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 os
import shutil
import subprocess
import tempfile
# set up: the same CPython 3.11 binary named python3.11m in two PATH directories
scene = tempfile.mkdtemp()
link_dir = os.path.join(scene, "link")
copy_dir = os.path.join(scene, "copy")
os.mkdir(link_dir)
os.mkdir(copy_dir)
os.symlink("/usr/bin/python3.11", os.path.join(link_dir, "python3.11m"))
shutil.copy("/usr/bin/python3.11", os.path.join(copy_dir, "python3.11m"))
# run: the same lookup once per directory, each in a fresh process (discovery caches results in-process);
# cwd is neither PATH dir, because _satisfies_path accepts os.path.abspath("python3.11m") before any name check
caller = ["/opt/venv/bin/python", "/tmp/repro.d/find_python3_11m.py"]
via_link = subprocess.run([*caller, link_dir], cwd=scene, capture_output=True, text=True, check=True)
via_copy = subprocess.run([*caller, copy_dir], cwd=scene, capture_output=True, text=True, check=True)
shutil.rmtree(scene)
print(f"PATH={link_dir} python3.11m -> /usr/bin/python3.11")
print(via_link.stderr, end="")
print(" get_interpreter returned:", via_link.stdout.strip())
print()
print(f"PATH={copy_dir} python3.11m is a copy of /usr/bin/python3.11")
print(via_copy.stderr, end="")
print(" get_interpreter returned:", via_copy.stdout.strip())
# check: an interpreter found under the requested name must be returned whether it is a symlink or a copy
link_rejected = via_link.stdout.strip() == "None"
copy_accepted = via_copy.stdout.strip() == os.path.join(copy_dir, "python3.11m")
print("BUG" if link_rejected and copy_accepted else "OK")repro.d/find_python3_11m.py
import logging
import sys
from python_discovery import get_interpreter
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr, format=" %(message)s")
found = get_interpreter("python3.11m", env={"PATH": sys.argv[1]})
print(found.executable if found else None)Output on 004294957ed227fe7e52b2864952ba09fd28b220:
PATH=/tmp/tmpoobpc4fq/link python3.11m -> /usr/bin/python3.11
find interpreter for spec PythonSpec(path=python3.11m)
discover PATH[0]=/tmp/tmpoobpc4fq/link
filesystem is case-sensitive
get interpreter info via cmd: /tmp/tmpoobpc4fq/link/python3.11m /src/src/python_discovery/_py_info_collect.py 30c8a2ed1504045e939f80d75f36c3a0 3600dd3f687acb75adc72fd93da7818b
get interpreter info via cmd: /usr/bin/python3.11 /src/src/python_discovery/_py_info_collect.py 263aec6c6ea950dd03a388e3f902e179 4cc2819ed863b696544d3a3276172495
proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpoobpc4fq/link/python3.11m, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
get_interpreter returned: None
PATH=/tmp/tmpoobpc4fq/copy python3.11m is a copy of /usr/bin/python3.11
find interpreter for spec PythonSpec(path=python3.11m)
discover PATH[0]=/tmp/tmpoobpc4fq/copy
filesystem is case-sensitive
get interpreter info via cmd: /tmp/tmpoobpc4fq/copy/python3.11m /src/src/python_discovery/_py_info_collect.py 1f7da2019cf95a2a464571fc6015d707 a7fdb7af5a810bd468fe94cdaa7adab6
proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, exe=/tmp/tmpoobpc4fq/copy/python3.11m, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
accepted PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, exe=/tmp/tmpoobpc4fq/copy/python3.11m, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
get_interpreter returned: /tmp/tmpoobpc4fq/copy/python3.11m
BUG
Expected: The symlink's name should satisfy a literal executable-name request.
Impact
Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is niche: the request must use a name outside the version-spec grammar and find it through a symlink. The reproduced effect stays within the caller's configured cache, search path, or interpreter.
Credits
Reported by Anthropic's OSS Scanner: ANT-2026-9225HCHJ.
Activity