diff --git a/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113716.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113716.patch new file mode 100644 index 0000000000..c19da2c11d --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113716.patch @@ -0,0 +1,208 @@ +From 554d9f9d4f7758f5cf53f993031e29787f1f74c5 Mon Sep 17 00:00:00 2001 +From: Bernát Gábor +Date: Sat, 17 Oct 2026 03:36:11 -0700 +Subject: [PATCH] fix(create): replace linked output directories (#3429) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +CVE: CVE-2026-113716 +Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/554d9f9d4f7758f5cf53f993031e29787f1f74c5] + +Backport Changes: +- Add the fs_supports_symlink import required by the upstream utility + tests; it is absent from the 21.2.1 test module. +- Extend the older test_util path import with ensure_within; the later + upstream module's safe_delete and symlink imports are unused here. +- Omit the release-only changelog fragment. + +Signed-off-by: Hetvi Thakar +--- + .../builtin/via_global_self_do.py | 4 +- + src/virtualenv/create/via_global_ref/venv.py | 7 ++- + tests/unit/create/test_creator.py | 36 +++++++++++++ + tests/unit/test_util.py | 54 ++++++++++++++++++- + 4 files changed, 94 insertions(+), 7 deletions(-) + +diff --git a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py +index f86861e..9ada4c1 100644 +--- a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py ++++ b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py +@@ -9,7 +9,7 @@ from virtualenv.create.via_global_ref.builtin.ref import ( + RefMust, + RefWhen, + ) +-from virtualenv.util.path import ensure_dir ++from virtualenv.util.path import ensure_within + + from .builtin_way import VirtualenvBuiltin + +@@ -92,7 +92,7 @@ class ViaGlobalRefVirtualenvBuiltin(ViaGlobalRefApi, VirtualenvBuiltin, ABC): + if any(i for i in dirs if i is not directory and directory.parts == i.parts[: len(directory.parts)]): + dirs.remove(directory) + for directory in sorted(dirs): +- ensure_dir(directory) ++ ensure_within(self.dest, directory) + + self.set_pyenv_cfg() + self.pyenv_cfg.write() +diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py +index 8b704eb..4e4fc77 100644 +--- a/src/virtualenv/create/via_global_ref/venv.py ++++ b/src/virtualenv/create/via_global_ref/venv.py +@@ -9,7 +9,7 @@ from python_discovery import PythonInfo + from virtualenv.create.pyenv_cfg import _one_line as collapse_line_boundaries + from virtualenv.create.via_global_ref.store import handle_store_python + from virtualenv.util.error import ProcessCallFailedError +-from virtualenv.util.path import ensure_dir, ensure_within ++from virtualenv.util.path import ensure_within + from virtualenv.util.subprocess import run_cmd + + from .api import ViaGlobalRefApi, ViaGlobalRefMeta +@@ -52,7 +52,8 @@ class Venv(ViaGlobalRefApi): + def create(self) -> None: + # stdlib venv writes interpreter files, configuration and activation scripts in place, so a planted symlink or + # hard link would receive them outside the environment +- ensure_within(self.dest, self.bin_dir) ++ for directory in (self.bin_dir, self.purelib, self.platlib): ++ ensure_within(self.dest, directory) + for path in (self.dest / "pyvenv.cfg", self.dest / ".gitignore", *self.bin_dir.iterdir()): + if path.is_symlink() or (path.is_file() and path.stat().st_nlink > 1): + path.unlink() +@@ -60,8 +61,6 @@ class Venv(ViaGlobalRefApi): + self.create_inline() + else: + self.create_via_sub_process() +- for lib in self.libs: # ty: ignore[not-iterable] +- ensure_dir(lib) + if self.describe is not None: + self.describe.install_venv_shared_libs(self) + super().create() +diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py +index bc02679..733a073 100644 +--- a/tests/unit/create/test_creator.py ++++ b/tests/unit/create/test_creator.py +@@ -220,6 +220,42 @@ def test_venv_creator_accepts_plain_destination(tmp_path: Path) -> None: + assert (session.creator.dest / "pyvenv.cfg").is_file() + + ++@pytest.mark.parametrize( ++ "junction", ++ [ ++ pytest.param( ++ False, id="symlink", marks=pytest.mark.skipif(not fs_supports_symlink(), reason="symlinks are unavailable") ++ ), ++ pytest.param(True, id="junction", marks=pytest.mark.skipif(os.name != "nt", reason="Windows junctions")), ++ ], ++) ++@pytest.mark.parametrize( ++ "creator_name", ++ [ ++ pytest.param("builtin", id="builtin"), ++ pytest.param( ++ "venv", ++ id="venv", ++ marks=pytest.mark.skipif("venv" not in CURRENT_CREATORS, reason="stdlib venv is unavailable"), ++ ), ++ ], ++) ++@pytest.mark.parametrize("folder", [pytest.param("bin_dir", id="executables"), pytest.param("purelib", id="packages")]) ++def test_creator_replaces_directory_links(tmp_path: Path, creator_name: str, folder: str, junction: bool) -> None: ++ session: Final = session_via_cli([str(tmp_path / "env"), "--creator", creator_name, "--no-seed"]) ++ session.creator.dest.mkdir() ++ stored: Final = session.creator.dest / "stored" ++ stored.mkdir() ++ link: Final = session.creator.dest / getattr(session.creator, folder).relative_to(session.creator.dest).parts[0] ++ if junction: ++ subprocess.run(["cmd", "/c", "mklink", "/J", str(link), str(stored)], capture_output=True, check=True) ++ else: ++ link.symlink_to(stored, target_is_directory=True) ++ with session: ++ session.run() ++ assert (link.resolve(), session.creator.exe.is_file(), list(stored.iterdir())) == (link, True, []) ++ ++ + CREATE_METHODS = [] + for k, v in CreatorSelector.for_interpreter(CURRENT).key_to_meta.items(): + if k in CURRENT_CREATORS: +diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py +index c4c448a..099798a 100644 +--- a/tests/unit/test_util.py ++++ b/tests/unit/test_util.py +@@ -2,14 +2,16 @@ from __future__ import annotations + + import concurrent.futures + import os ++import subprocess + import traceback + from typing import TYPE_CHECKING, Final + + import pytest + + from virtualenv.app_data import _cache_dir_with_migration, _default_app_data_dir ++from virtualenv.info import fs_supports_symlink + from virtualenv.util.lock import ReentrantFileLock +-from virtualenv.util.path import copy, write_file ++from virtualenv.util.path import copy, ensure_within, write_file + from virtualenv.util.subprocess import run_cmd + + if TYPE_CHECKING: +@@ -69,6 +71,56 @@ def test_write_file_replaces_content(tmp_path: Path, existing: bool) -> None: + assert path.read_bytes() == b"generated" + + ++@pytest.mark.parametrize("existing", [pytest.param(False, id="new"), pytest.param(True, id="existing")]) ++def test_environment_directories_create_nested_path(tmp_path: Path, existing: bool) -> None: ++ folder: Final = tmp_path / "env" / "lib" / "python" ++ if existing: ++ folder.mkdir(parents=True) ++ ensure_within(tmp_path / "env", folder) ++ assert folder.is_dir() ++ ++ ++def test_environment_directories_reject_regular_file(tmp_path: Path) -> None: ++ folder: Final = tmp_path / "file" ++ folder.touch() ++ with pytest.raises(NotADirectoryError, match="not a real directory"): ++ ensure_within(tmp_path, folder) ++ ++ ++def test_environment_directories_reject_parent_components(tmp_path: Path) -> None: ++ with pytest.raises(ValueError, match="must remain inside"): ++ ensure_within(tmp_path, tmp_path / ".." / "folder") ++ ++ ++@pytest.mark.skipif(not fs_supports_symlink(), reason="symlinks are unavailable") ++@pytest.mark.parametrize( ++ "existing", [pytest.param(False, id="missing-target"), pytest.param(True, id="existing-target")] ++) ++def test_environment_directories_remove_links(tmp_path: Path, caplog: pytest.LogCaptureFixture, existing: bool) -> None: ++ target: Final = tmp_path / "stored" ++ if existing: ++ target.mkdir() ++ link: Final = tmp_path / "link" ++ link.symlink_to(target, target_is_directory=True) ++ ensure_within(tmp_path, link / "nested") ++ assert ( ++ link.is_symlink(), ++ link.exists(), ++ target.exists(), ++ f"replace environment directory link {link}" in caplog.text, ++ ) == (False, True, existing, True) ++ ++ ++@pytest.mark.skipif(os.name != "nt", reason="Windows junctions") ++def test_environment_directories_report_replaced_junction(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: ++ target: Final = tmp_path / "stored" ++ target.mkdir() ++ link: Final = tmp_path / "link" ++ subprocess.run(["cmd", "/c", "mklink", "/J", str(link), str(target)], check=True, capture_output=True) ++ ensure_within(tmp_path, link) ++ assert (link.resolve(), f"replace environment directory junction {link}" in caplog.text) == (link, True) ++ ++ + def test_run_fail(tmp_path) -> None: + code, out, err = run_cmd([str(tmp_path)]) + assert err +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb b/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb index bc31885a02..1a60c04c44 100644 --- a/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb +++ b/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb @@ -27,6 +27,7 @@ SRC_URI += " \ file://CVE-2026-113720.patch \ file://CVE-2026-113719.patch \ file://CVE-2026-113721.patch \ + file://CVE-2026-113716.patch \ " BBCLASSEXTEND = "native nativesdk"