Skip to content

Base-interpreter probes ignore the supplied environment #193

Closed

Description

@gaborbernat

A lookup with an explicit environment uses it for the first interpreter probe but not for the base-interpreter probe. The returned metadata can therefore reflect the process environment instead.

Affected code

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

Mechanism

resolve_to_system() invokes the follow-up probe without passing the caller's env mapping.

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 tempfile

# set up: a venv of /usr/bin/python3.11; the caller asks for its python by path with
# env={"PATH": "/usr/bin", "PY_DISCOVERY_TIMEOUT": "10"}
root = tempfile.mkdtemp()
venv = os.path.join(root, "venv")
subprocess.run(["/usr/bin/python3.11", "-m", "venv", "--without-pip", venv], check=True)
caller = [
    "/opt/venv/bin/python",
    "/tmp/repro.d/get_interpreter_with_env_mapping.py",
    os.path.join(venv, "bin", "python"),
]

# run 1: PYTHONPATH is in the process environment only, not in the mapping
process_only_dir = os.path.join(root, "only-in-process-environ")
run1 = subprocess.run(caller, env={"PYTHONPATH": process_only_dir}, capture_output=True, text=True, check=True)
info = json.loads(run1.stdout)
print("run 1 executable       :", info["executable"])
print("run 1 system_executable:", info["system_executable"])
print("run 1 path             :", info["path"])

# run 2: the process environment says 1 ms per probe, the mapping says 10 s
run2 = subprocess.run(caller, env={"PY_DISCOVERY_TIMEOUT": "0.001"}, capture_output=True, text=True)
print("run 2 exit status      :", run2.returncode)
print("run 2 last line        :", (run2.stdout + run2.stderr).strip().splitlines()[-1])

# check: had the mapping been used for the base probe, run 1's path would not hold
# process_only_dir and run 2 would return the venv like run 1 did
base_probe_got_process_pythonpath = (
    info["system_executable"] == "/usr/bin/python3.11" and process_only_dir in info["path"]
)
base_probe_got_process_timeout = "failed to query /usr/bin/python3.11 with code -1 err: 'timed out'" in run2.stderr
print("base probe saw the process PYTHONPATH       :", base_probe_got_process_pythonpath)
print("base probe was cut off by the process's 1 ms:", base_probe_got_process_timeout)
print("BUG" if base_probe_got_process_pythonpath and base_probe_got_process_timeout else "OK")

repro.d/get_interpreter_with_env_mapping.py

import json
import sys

from python_discovery import get_interpreter

env = {"PATH": "/usr/bin", "PY_DISCOVERY_TIMEOUT": "10"}
info = get_interpreter(sys.argv[1], env=env)
print(json.dumps({"executable": info.executable, "system_executable": info.system_executable, "path": info.path}))

Output on 004294957ed227fe7e52b2864952ba09fd28b220:

run 1 executable       : /tmp/tmp7511fxsr/venv/bin/python
run 1 system_executable: /usr/bin/python3.11
run 1 path             : ['/src/src/python_discovery', '/tmp/tmp7511fxsr/only-in-process-environ', '/usr/lib/python311.zip', '/usr/lib/python3.11', '/usr/lib/python3.11/lib-dynload', '/usr/local/lib/python3.11/dist-packages', '/usr/lib/python3/dist-packages']
run 2 exit status      : 1
run 2 last line        : RuntimeError: failed to query /usr/bin/python3.11 with code -1 err: 'timed out'
base probe saw the process PYTHONPATH       : True
base probe was cut off by the process's 1 ms: True
BUG

Expected: Both probes should use the supplied mapping, including its timeout setting.

Impact

Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is niche: the candidate must be a venv or symlink and the supplied mapping must differ from os.environ. The reproduced effect stays within the caller's configured cache, search path, or interpreter.

Credits

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

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