From 2b2a99f49d8030e34e0b45e998df35c27b782d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 15:12:35 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(probe):=20use=20a=20priv?= =?UTF-8?q?ate=20temp=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Archive imports extracted the interrogation script into the shared temp root, which Python adds to the probe's module search path. Place the script in a private directory and remove that directory after the probe. --- src/python_discovery/_cached_py_info.py | 12 +++++------- tests/test_cached_py_info.py | 5 ++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/python_discovery/_cached_py_info.py b/src/python_discovery/_cached_py_info.py index 0a28cf3..d9aff86 100644 --- a/src/python_discovery/_cached_py_info.py +++ b/src/python_discovery/_cached_py_info.py @@ -195,13 +195,11 @@ def _resolve_py_info_script() -> Generator[Path]: if data is None: msg = f"cannot locate {_PY_INFO_SCRIPT.name} for subprocess interrogation" raise FileNotFoundError(msg) - fd, tmp = tempfile.mkstemp(suffix=".py") - try: - os.write(fd, data) - os.close(fd) - yield Path(tmp) - finally: - Path(tmp).unlink() + # Python prepends the probe script's directory to sys.path, so keep that directory private. + with tempfile.TemporaryDirectory() as private_dir: + script: Final[Path] = Path(private_dir) / _PY_INFO_SCRIPT.name + script.write_bytes(data) + yield script def _extract_between_cookies(out: str, start_cookie: str, end_cookie: str) -> tuple[str, str, int, int]: diff --git a/tests/test_cached_py_info.py b/tests/test_cached_py_info.py index feac837..04bd5ba 100644 --- a/tests/test_cached_py_info.py +++ b/tests/test_cached_py_info.py @@ -4,6 +4,7 @@ import logging import os import sys +import tempfile from pathlib import Path from subprocess import TimeoutExpired, list2cmdline from typing import TYPE_CHECKING, Final @@ -59,7 +60,9 @@ def test_resolve_py_info_script_fallback_to_pkgutil(mocker: MockerFixture) -> No assert script.exists() content = script.read_text(encoding="utf-8") assert content == "# mock script" - assert not script.exists() + assert script.parent != Path(tempfile.gettempdir()) + assert list(script.parent.iterdir()) == [script] + assert not script.parent.exists() def test_resolve_py_info_script_pkgutil_returns_none(mocker: MockerFixture) -> None: From d6c6b935fc1b18840c874e03738eb76c96cd1558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 15:13:06 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs(news):=20record=20priva?= =?UTF-8?q?te=20probe=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/206.security.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/changelog/206.security.rst diff --git a/docs/changelog/206.security.rst b/docs/changelog/206.security.rst new file mode 100644 index 0000000..ec8c3f5 --- /dev/null +++ b/docs/changelog/206.security.rst @@ -0,0 +1,2 @@ +When imported from an archive, python-discovery now extracts its interpreter probe into a private temporary directory. +This prevents the probe from importing modules supplied in the shared temp directory.