Skip to content

PyPy requests can select a CPython with a PyPy filename #201

Closed

Description

@gaborbernat

A PyPy spec can accept a CPython executable named pypy3.11. The returned PythonInfo says CPython, but the lookup reports it as satisfying the PyPy request.

Affected code

src/python_discovery/_discovery.py:478 at the reported commit. The reproducer also returns BUG on current main 004294957ed227fe7e52b2864952ba09fd28b220.

Mechanism

The path-name filter admits the filename and the later acceptance check does not reject the interpreter's different implementation.

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/repro

repro

#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import tempfile

CPYTHON = "/usr/bin/python3.11"
REQUEST = "pypy3.11"
NAMES_ON_PATH = ["python3.11", "pypy3.11", "Python3.11"]  # python3.11 is the control

# set up: the stand-in interpreter is a CPython
stand_in = subprocess.run(
    [CPYTHON, "-c", "import platform; print(platform.python_implementation())"],
    capture_output=True,
    text=True,
    check=True,
).stdout.strip()
print(f"{CPYTHON} is {stand_in}")
scratch = tempfile.mkdtemp()

# trigger: the same CPython alone on PATH under each name, pypy3.11 requested, one fresh process and cache each
# (fresh processes, because python-discovery also caches interpreter info in memory)
selected = {}
for name in NAMES_ON_PATH:
    path_dir = os.path.join(scratch, name + "-bin")
    os.mkdir(path_dir)
    os.symlink(CPYTHON, os.path.join(path_dir, name))
    cache_dir = os.path.join(scratch, name + "-cache")

    call = subprocess.run(
        [sys.executable, "/tmp/repro.d/find_interpreter.py", REQUEST, path_dir, cache_dir],
        capture_output=True,
        text=True,
        check=True,
    )
    print(f"\nPATH={path_dir}  ({name} -> {CPYTHON})  get_interpreter({REQUEST!r}) log:")
    print(call.stderr, end="")
    selected[name] = json.loads(call.stdout)

# check: a pypy3.11 request must return a PyPy or None, whatever the file on PATH is called
print()
for name, info in selected.items():
    print(f"{name:>10} on PATH -> {info}")
not_pypy = [name for name, info in selected.items() if info is not None and info["implementation"] != "PyPy"]
print("BUG" if not_pypy else "OK")

repro.d/find_interpreter.py

import json
import logging
import sys
from pathlib import Path

from python_discovery import DiskCache, get_interpreter

spec, path_dir, cache_dir = sys.argv[1:]

# shows every interrogation argv and the proposed/accepted decisions on stderr
logging.basicConfig(format="    %(message)s")
logging.getLogger("python_discovery").setLevel(logging.DEBUG)

info = get_interpreter(spec, cache=DiskCache(Path(cache_dir)), env={"PATH": path_dir})

if info is None:
    print(json.dumps(None))
else:
    print(
        json.dumps({
            "executable": info.executable,
            "system_executable": info.system_executable,
            "implementation": info.implementation,
            "spec": info.spec,
        })
    )

Output on 004294957ed227fe7e52b2864952ba09fd28b220:

/usr/bin/python3.11 is CPython

PATH=/tmp/tmpfvw5s9vi/python3.11-bin  (python3.11 -> /usr/bin/python3.11)  get_interpreter('pypy3.11') log:
    find interpreter for spec PythonSpec(implementation=pypy, major=3, minor=11, free_threaded=False)
    get interpreter info via cmd: /usr/local/bin/python3.15 /src/src/python_discovery/_py_info_collect.py f301952094e71991d774bdb741721d9b 6086cf8e271bd3172584765d27535c1f
    wrote python info at /tmp/tmpfvw5s9vi/python3.11-cache/py_info/4/755e5f8e9348854516c9513236b846adb15e9a21490fd9dc391537946af27457.json
    filesystem is case-sensitive
    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)
    discover PATH[0]=/tmp/tmpfvw5s9vi/python3.11-bin
    get interpreter info via cmd: /tmp/tmpfvw5s9vi/python3.11-bin/python3.11 /src/src/python_discovery/_py_info_collect.py 89a120178e873ae144e973b7d436a3fd ecf37618ad4e11b62734237932b85d45
    wrote python info at /tmp/tmpfvw5s9vi/python3.11-cache/py_info/4/a1586e0a50513ab4d5ca39158fe53d239d45b28a4d4c232f2263117a562cca9e.json
    get interpreter info via cmd: /usr/bin/python3.11 /src/src/python_discovery/_py_info_collect.py 6f26471b56546ff7290cdd53c51938ac 8b484f3ebf626ec7e1af6559a7c0e2ad
    wrote python info at /tmp/tmpfvw5s9vi/python3.11-cache/py_info/4/ca3ed784184f1b3bb7c3539bfb45e71710cd27667424f92c2d5bb4df9c107c23.json
    proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/python3.11-bin/python3.11, platform=linux, version='3.11.2 (main, Oct  7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)

PATH=/tmp/tmpfvw5s9vi/pypy3.11-bin  (pypy3.11 -> /usr/bin/python3.11)  get_interpreter('pypy3.11') log:
    find interpreter for spec PythonSpec(implementation=pypy, major=3, minor=11, free_threaded=False)
    get interpreter info via cmd: /usr/local/bin/python3.15 /src/src/python_discovery/_py_info_collect.py 83b7f186188cd30612c6e4d2d00b9583 e22ea2e6a429f834924ccfca7f06bf7b
    wrote python info at /tmp/tmpfvw5s9vi/pypy3.11-cache/py_info/4/755e5f8e9348854516c9513236b846adb15e9a21490fd9dc391537946af27457.json
    filesystem is case-sensitive
    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)
    discover PATH[0]=/tmp/tmpfvw5s9vi/pypy3.11-bin
    get interpreter info via cmd: /tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11 /src/src/python_discovery/_py_info_collect.py 9fcfe3d845c385441bc6d1b380883f13 bfa1536c692e6874a43ac6c520ae0043
    wrote python info at /tmp/tmpfvw5s9vi/pypy3.11-cache/py_info/4/88774105092c9778ffe9cc8db2f726a53d66e92f7abd57c1f26faf142e5f3065.json
    get interpreter info via cmd: /usr/bin/python3.11 /src/src/python_discovery/_py_info_collect.py 5d4342aec3bdafcde1b509d64dbf88c9 5c1bafa4314d474bb2b870f574689bc0
    wrote python info at /tmp/tmpfvw5s9vi/pypy3.11-cache/py_info/4/ca3ed784184f1b3bb7c3539bfb45e71710cd27667424f92c2d5bb4df9c107c23.json
    proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11, 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, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11, platform=linux, version='3.11.2 (main, Oct  7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)

PATH=/tmp/tmpfvw5s9vi/Python3.11-bin  (Python3.11 -> /usr/bin/python3.11)  get_interpreter('pypy3.11') log:
    find interpreter for spec PythonSpec(implementation=pypy, major=3, minor=11, free_threaded=False)
    get interpreter info via cmd: /usr/local/bin/python3.15 /src/src/python_discovery/_py_info_collect.py 582d82a615af9f8e71569468bcd0ab3d a58736cb88b725298c9e0a0892ba10c8
    wrote python info at /tmp/tmpfvw5s9vi/Python3.11-cache/py_info/4/755e5f8e9348854516c9513236b846adb15e9a21490fd9dc391537946af27457.json
    filesystem is case-sensitive
    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)
    discover PATH[0]=/tmp/tmpfvw5s9vi/Python3.11-bin
    get interpreter info via cmd: /tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11 /src/src/python_discovery/_py_info_collect.py afd7b421f8a64a84651f61ef58d3167c 08a5ba309be2b3fe3319181896831406
    wrote python info at /tmp/tmpfvw5s9vi/Python3.11-cache/py_info/4/a64e97bd7b6718c8c60e29fd10f4fefe0364968f0468a157dabcaf90244b7521.json
    get interpreter info via cmd: /usr/bin/python3.11 /src/src/python_discovery/_py_info_collect.py d13b2277dfd877d7e2be17f89615f328 44502ea8ca495d93333c6d17b5ae4915
    wrote python info at /tmp/tmpfvw5s9vi/Python3.11-cache/py_info/4/ca3ed784184f1b3bb7c3539bfb45e71710cd27667424f92c2d5bb4df9c107c23.json
    proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11, 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, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11, platform=linux, version='3.11.2 (main, Oct  7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)

python3.11 on PATH -> None
  pypy3.11 on PATH -> {'executable': '/tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11', 'system_executable': '/usr/bin/python3.11', 'implementation': 'CPython', 'spec': 'CPython3.11.2.final.0-64-x86_64'}
Python3.11 on PATH -> {'executable': '/tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11', 'system_executable': '/usr/bin/python3.11', 'implementation': 'CPython', 'spec': 'CPython3.11.2.final.0-64-x86_64'}
BUG

Expected: A PyPy spec should reject an interpreter that reports CPython.

Impact

Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is niche: a CPython executable must sit on PATH under a PyPy-like filename. The reproduced effect stays within the caller's configured cache, search path, or interpreter.

Credits

Reported by Anthropic's OSS Scanner: ANT-2026-Y51C67N2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions