mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-13 15:47:23 +00:00
python3-virtualenv: fix CVE-2026-113721
This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Replace existing symbolic and hard links before writing generated files, and create the replacement files exclusively. Prepare the stdlib venv executable directory before it writes files. [1] https://github.com/pypa/virtualenv/commit/d25f28699eefb1fc34a251e5adf2b5de844c36cd [2] https://nvd.nist.gov/vuln/detail/CVE-2026-113721 Signed-off-by: Hetvi Thakar <hthakar@cisco.com> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
1 parent
90f0e58d3d
commit
15567524b8
2 files changed
+478
No files matched your search
@@ -0,0 +1,477 @@
|
||||
From d25f28699eefb1fc34a251e5adf2b5de844c36cd Mon Sep 17 00:00:00 2001
|
||||
From: Bernát Gábor <gaborjbernat@gmail.com>
|
||||
Date: Sat, 17 Oct 2026 03:25:09 -0700
|
||||
Subject: [PATCH] fix(create): replace generated file links (#3431)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
|
||||
|
||||
CVE: CVE-2026-113721
|
||||
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/d25f28699eefb1fc34a251e5adf2b5de844c36cd]
|
||||
|
||||
Backport Changes:
|
||||
- Replace PyEnvCfg's older write_text() call instead of write_bytes();
|
||||
retain _one_line() serialization and separate the new helper import
|
||||
from the type-only imports, without the later util.text refactor.
|
||||
- Add the Windows junction-constant guard absent from 21.2.1's helper
|
||||
module and retain S_IWUSR for its existing deletion callback; do not
|
||||
import the later read-only deletion refactor.
|
||||
- Add the missing fs_supports_symlink test import and extend the older
|
||||
test_util path-import list with write_file.
|
||||
- Omit the release-only changelog fragment; the release documentation
|
||||
directory is absent from the 21.2.1 source distribution.
|
||||
|
||||
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||
---
|
||||
src/virtualenv/activation/via_template.py | 21 ++-
|
||||
src/virtualenv/create/creator.py | 6 +-
|
||||
src/virtualenv/create/pyenv_cfg.py | 4 +-
|
||||
src/virtualenv/create/via_global_ref/api.py | 5 +-
|
||||
src/virtualenv/create/via_global_ref/venv.py | 8 +-
|
||||
.../embed/via_app_data/pip_install/base.py | 4 +
|
||||
src/virtualenv/util/path/__init__.py | 4 +-
|
||||
src/virtualenv/util/path/_sync.py | 46 ++++++-
|
||||
tests/unit/create/test_creator.py | 127 +++++++++++++++++-
|
||||
tests/unit/test_util.py | 11 +-
|
||||
10 files changed, 213 insertions(+), 23 deletions(-)
|
||||
|
||||
diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py
|
||||
index 3af9dc5..3fba6ad 100644
|
||||
--- a/src/virtualenv/activation/via_template.py
|
||||
+++ b/src/virtualenv/activation/via_template.py
|
||||
@@ -5,7 +5,9 @@ import re
|
||||
import shlex
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import TYPE_CHECKING, Final
|
||||
+
|
||||
+from virtualenv.util.path import write_file
|
||||
|
||||
from .activator import Activator
|
||||
|
||||
@@ -63,18 +65,13 @@ class ViaTemplateActivator(Activator, ABC):
|
||||
def _generate(
|
||||
self, replacements: dict[str, str], templates: Iterable[str], to_folder: Path, creator: Creator
|
||||
) -> list[Path]:
|
||||
- generated = []
|
||||
+ generated: Final[list[Path]] = []
|
||||
for template in templates:
|
||||
- text = self.instantiate_template(replacements, template, creator)
|
||||
- dest = to_folder / self.as_name(template)
|
||||
- # remove the file if it already exists - this prevents permission
|
||||
- # errors when the dest is not writable
|
||||
- if dest.exists():
|
||||
- dest.unlink()
|
||||
- # Powershell assumes Windows 1252 encoding when reading files without BOM
|
||||
- encoding = "utf-8-sig" if str(template).endswith(".ps1") else "utf-8"
|
||||
- # use write_bytes to avoid platform specific line normalization (\n -> \r\n)
|
||||
- dest.write_bytes(text.encode(encoding))
|
||||
+ text: Final = self.instantiate_template(replacements, template, creator)
|
||||
+ dest: Final = to_folder / self.as_name(template)
|
||||
+ # PowerShell assumes Windows 1252 when the UTF-8 BOM is absent.
|
||||
+ encoding: Final = "utf-8-sig" if str(template).endswith(".ps1") else "utf-8"
|
||||
+ write_file(dest, text.encode(encoding))
|
||||
generated.append(dest)
|
||||
return generated
|
||||
|
||||
diff --git a/src/virtualenv/create/creator.py b/src/virtualenv/create/creator.py
|
||||
index 9fa60d3..855d5a1 100644
|
||||
--- a/src/virtualenv/create/creator.py
|
||||
+++ b/src/virtualenv/create/creator.py
|
||||
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from os.path import commonpath
|
||||
|
||||
-from virtualenv.util.path import safe_delete
|
||||
+from virtualenv.util.path import safe_delete, write_file
|
||||
from virtualenv.util.subprocess import LogCmd, run_cmd
|
||||
from virtualenv.version import __version__
|
||||
|
||||
@@ -215,7 +215,7 @@ class Creator(ABC):
|
||||
# For information about cache directory tags, see:
|
||||
# https://bford.info/cachedir/
|
||||
""").strip()
|
||||
- cachedir_tag_file.write_text(cachedir_tag_text, encoding="utf-8")
|
||||
+ write_file(cachedir_tag_file, cachedir_tag_text.encode("utf-8"))
|
||||
|
||||
def set_pyenv_cfg(self) -> None:
|
||||
self.pyenv_cfg.content = OrderedDict()
|
||||
@@ -237,7 +237,7 @@ class Creator(ABC):
|
||||
# mark this folder to be ignored by VCS, handle https://www.python.org/dev/peps/pep-0610/#registered-vcs
|
||||
git_ignore = self.dest / ".gitignore"
|
||||
if not git_ignore.exists():
|
||||
- git_ignore.write_text("# created by virtualenv automatically\n*\n", encoding="utf-8")
|
||||
+ write_file(git_ignore, b"# created by virtualenv automatically\n*\n")
|
||||
# Mercurial - does not support the .hgignore file inside a subdirectory directly, but only if included via the
|
||||
# subinclude directive from root, at which point on might as well ignore the directory itself, see
|
||||
# https://www.selenic.com/mercurial/hgignore.5.html for more details
|
||||
diff --git a/src/virtualenv/create/pyenv_cfg.py b/src/virtualenv/create/pyenv_cfg.py
|
||||
index 52411fb..c0fb345 100644
|
||||
--- a/src/virtualenv/create/pyenv_cfg.py
|
||||
+++ b/src/virtualenv/create/pyenv_cfg.py
|
||||
@@ -5,6 +5,8 @@ import os
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
+from virtualenv.util.path import write_file
|
||||
+
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -67,7 +69,7 @@ class PyEnvCfg:
|
||||
LOGGER.debug("\t%s", line)
|
||||
text += line
|
||||
text += "\n"
|
||||
- self.path.write_text(text, encoding="utf-8")
|
||||
+ write_file(self.path, text.encode("utf-8"))
|
||||
|
||||
def refresh(self) -> OrderedDict[str, str]:
|
||||
self.content = self._read_values(self.path)
|
||||
diff --git a/src/virtualenv/create/via_global_ref/api.py b/src/virtualenv/create/via_global_ref/api.py
|
||||
index 5cb8e12..5382419 100644
|
||||
--- a/src/virtualenv/create/via_global_ref/api.py
|
||||
+++ b/src/virtualenv/create/via_global_ref/api.py
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.create.creator import Creator, CreatorMeta
|
||||
from virtualenv.info import fs_supports_symlink
|
||||
+from virtualenv.util.path import write_file
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import ArgumentParser
|
||||
@@ -116,10 +117,10 @@ class ViaGlobalRefApi(Creator, ABC):
|
||||
if text:
|
||||
pth = self.purelib / "_virtualenv.pth"
|
||||
LOGGER.debug("create virtualenv import hook file %s", pth)
|
||||
- pth.write_text("import _virtualenv", encoding="utf-8")
|
||||
+ write_file(pth, b"import _virtualenv")
|
||||
dest_path = self.purelib / "_virtualenv.py"
|
||||
LOGGER.debug("create %s", dest_path)
|
||||
- dest_path.write_text(text, encoding="utf-8")
|
||||
+ write_file(dest_path, text.encode("utf-8"))
|
||||
|
||||
def env_patch_text(self) -> str:
|
||||
"""Patch the distutils package to not be derailed by its configuration files."""
|
||||
diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py
|
||||
index 2bfd1b4..8b704eb 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
|
||||
+from virtualenv.util.path import ensure_dir, ensure_within
|
||||
from virtualenv.util.subprocess import run_cmd
|
||||
|
||||
from .api import ViaGlobalRefApi, ViaGlobalRefMeta
|
||||
@@ -50,6 +50,12 @@ class Venv(ViaGlobalRefApi):
|
||||
return None
|
||||
|
||||
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 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()
|
||||
if self.can_be_inline:
|
||||
self.create_inline()
|
||||
else:
|
||||
diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/base.py b/src/virtualenv/seed/embed/via_app_data/pip_install/base.py
|
||||
index 7697e73..086ae0c 100644
|
||||
--- a/src/virtualenv/seed/embed/via_app_data/pip_install/base.py
|
||||
+++ b/src/virtualenv/seed/embed/via_app_data/pip_install/base.py
|
||||
@@ -219,6 +219,10 @@ class ScriptMakerCustom(ScriptMaker):
|
||||
self, names: set[str], shebang: bytes, script_bytes: bytes, filenames: list[str], ext: str
|
||||
) -> None:
|
||||
names.add(f"{self._name}{self.version_info[0]}.{self.version_info[1]}")
|
||||
+ for name in names:
|
||||
+ path: Final = Path(self.target_dir) / (f"{name}.exe" if os.name == "nt" else name)
|
||||
+ if path.is_symlink():
|
||||
+ path.unlink()
|
||||
super()._write_script(names, shebang, script_bytes, filenames, ext)
|
||||
|
||||
|
||||
diff --git a/src/virtualenv/util/path/__init__.py b/src/virtualenv/util/path/__init__.py
|
||||
index c144d8d..451630f 100644
|
||||
--- a/src/virtualenv/util/path/__init__.py
|
||||
+++ b/src/virtualenv/util/path/__init__.py
|
||||
@@ -1,16 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._permission import make_exe, set_tree
|
||||
-from ._sync import copy, copytree, ensure_dir, safe_delete, symlink
|
||||
+from ._sync import copy, copytree, ensure_dir, ensure_within, safe_delete, symlink, write_file
|
||||
from ._win import get_short_path_name
|
||||
|
||||
__all__ = [
|
||||
"copy",
|
||||
"copytree",
|
||||
"ensure_dir",
|
||||
+ "ensure_within",
|
||||
"get_short_path_name",
|
||||
"make_exe",
|
||||
"safe_delete",
|
||||
"set_tree",
|
||||
"symlink",
|
||||
+ "write_file",
|
||||
]
|
||||
diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py
|
||||
index a94466f..fae3859 100644
|
||||
--- a/src/virtualenv/util/path/_sync.py
|
||||
+++ b/src/virtualenv/util/path/_sync.py
|
||||
@@ -5,9 +5,12 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
-from stat import S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR
|
||||
+from stat import S_ISDIR, S_ISLNK, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR
|
||||
from typing import Final
|
||||
|
||||
+if os.name == "nt":
|
||||
+ from stat import IO_REPARSE_TAG_MOUNT_POINT
|
||||
+
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -17,6 +20,45 @@ def ensure_dir(path: Path) -> None:
|
||||
os.makedirs(str(path))
|
||||
|
||||
|
||||
+def ensure_within(root: Path, path: Path) -> None:
|
||||
+ parts: Final = path.relative_to(root).parts
|
||||
+ if ".." in parts:
|
||||
+ msg: Final = f"environment directory {path} must remain inside {root}"
|
||||
+ raise ValueError(msg)
|
||||
+ ensure_dir(root)
|
||||
+ folder = root
|
||||
+ for part in parts:
|
||||
+ folder /= part
|
||||
+ try:
|
||||
+ metadata: Final = folder.lstat()
|
||||
+ except FileNotFoundError:
|
||||
+ pass
|
||||
+ else:
|
||||
+ if S_ISLNK(metadata.st_mode):
|
||||
+ LOGGER.warning("replace environment directory link %s", folder)
|
||||
+ folder.unlink()
|
||||
+ elif os.name == "nt" and metadata.st_reparse_tag == IO_REPARSE_TAG_MOUNT_POINT:
|
||||
+ LOGGER.warning("replace environment directory junction %s", folder)
|
||||
+ folder.rmdir()
|
||||
+ try:
|
||||
+ folder.mkdir()
|
||||
+ except FileExistsError:
|
||||
+ created: Final = folder.lstat()
|
||||
+ if not S_ISDIR(created.st_mode) or (
|
||||
+ os.name == "nt" and created.st_reparse_tag == IO_REPARSE_TAG_MOUNT_POINT
|
||||
+ ):
|
||||
+ msg: Final = f"environment directory is not a real directory: {folder}"
|
||||
+ raise NotADirectoryError(msg) from None
|
||||
+
|
||||
+
|
||||
+def write_file(path: Path, data: bytes) -> None:
|
||||
+ if path.is_symlink() or path.exists():
|
||||
+ path.unlink()
|
||||
+ # Exclusive creation rejects links introduced after the old entry was removed.
|
||||
+ with path.open("xb") as stream:
|
||||
+ stream.write(data)
|
||||
+
|
||||
+
|
||||
def ensure_safe_to_do(src: Path, dest: Path) -> None:
|
||||
if src == dest:
|
||||
msg = f"source and destination is the same {src}"
|
||||
@@ -90,6 +132,8 @@ __all__ = [
|
||||
"copy",
|
||||
"copytree",
|
||||
"ensure_dir",
|
||||
+ "ensure_within",
|
||||
"safe_delete",
|
||||
"symlink",
|
||||
+ "write_file",
|
||||
]
|
||||
diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py
|
||||
index bd0f071..bc02679 100644
|
||||
--- a/tests/unit/create/test_creator.py
|
||||
+++ b/tests/unit/create/test_creator.py
|
||||
@@ -30,13 +30,101 @@ from virtualenv.create.pyenv_cfg import PyEnvCfg
|
||||
from virtualenv.create.via_global_ref import api
|
||||
from virtualenv.create.via_global_ref.builtin.cpython.common import is_mac_os_framework, is_macos_brew
|
||||
from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix
|
||||
-from virtualenv.info import IS_PYPY, IS_WIN, fs_is_case_sensitive
|
||||
+from virtualenv.info import IS_GRAALPY, IS_PYPY, IS_WIN, fs_is_case_sensitive, fs_supports_symlink
|
||||
from virtualenv.run import cli_run, session_via_cli
|
||||
from virtualenv.run.plugin.creators import CreatorSelector
|
||||
|
||||
CURRENT = PythonInfo.current_system()
|
||||
|
||||
|
||||
+@pytest.mark.parametrize("creator_name", [pytest.param("builtin", id="builtin"), pytest.param("venv", id="venv")])
|
||||
+@pytest.mark.parametrize("existing", [pytest.param(False, id="dangling"), pytest.param(True, id="existing")])
|
||||
+@pytest.mark.parametrize(
|
||||
+ "generated",
|
||||
+ [
|
||||
+ pytest.param(name, id=name)
|
||||
+ for name in ("pyvenv.cfg", "activate", "pip", "CACHEDIR.TAG", ".gitignore", "_virtualenv.py", "_virtualenv.pth")
|
||||
+ ],
|
||||
+)
|
||||
+def test_creator_replaces_generated_file_links(
|
||||
+ tmp_path: Path,
|
||||
+ creator_name: str,
|
||||
+ existing: bool,
|
||||
+ generated: str,
|
||||
+) -> None:
|
||||
+ command: Final = [
|
||||
+ str(tmp_path / "env"),
|
||||
+ "--creator",
|
||||
+ creator_name,
|
||||
+ "--pip",
|
||||
+ "embed",
|
||||
+ "--no-periodic-update",
|
||||
+ ]
|
||||
+ if generated.startswith("_virtualenv"):
|
||||
+ if (python39 := shutil.which("python3.9")) is None:
|
||||
+ pytest.skip("Python 3.9 is unavailable")
|
||||
+ command.extend(["--python", python39])
|
||||
+ session: Final = session_via_cli(command)
|
||||
+ session.creator.bin_dir.mkdir(parents=True)
|
||||
+ target: Final = session.creator.dest / "original"
|
||||
+ if existing:
|
||||
+ target.write_bytes(b"original")
|
||||
+ path = (
|
||||
+ session.creator.bin_dir / ("pip.exe" if generated == "pip" and os.name == "nt" else generated)
|
||||
+ if generated in {"activate", "pip"}
|
||||
+ else session.creator.dest / generated
|
||||
+ )
|
||||
+ if generated.startswith("_virtualenv"):
|
||||
+ path = session.creator.purelib / generated
|
||||
+ path.parent.mkdir(parents=True, exist_ok=True)
|
||||
+ path.symlink_to(target)
|
||||
+ with session:
|
||||
+ session.run()
|
||||
+ if existing and (generated == "CACHEDIR.TAG" or (generated == ".gitignore" and creator_name == "builtin")):
|
||||
+ assert path.is_symlink()
|
||||
+ else:
|
||||
+ assert not path.is_symlink()
|
||||
+ assert (target.read_bytes() if existing else target.exists()) == (b"original" if existing else False)
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize(
|
||||
+ "kind",
|
||||
+ [
|
||||
+ pytest.param("file", id="file"),
|
||||
+ pytest.param("symlink", id="symlink"),
|
||||
+ pytest.param("dangling", id="dangling"),
|
||||
+ pytest.param("junction", id="junction"),
|
||||
+ ],
|
||||
+)
|
||||
+def test_venv_prepares_real_executable_directory(tmp_path: Path, kind: str) -> None:
|
||||
+ session: Final = session_via_cli([str(tmp_path / "env"), "--creator", "venv", "--no-seed", "--activators="])
|
||||
+ session.creator.dest.mkdir()
|
||||
+ if kind == "file":
|
||||
+ session.creator.bin_dir.write_bytes(b"file")
|
||||
+ else:
|
||||
+ target: Final = session.creator.dest / "empty"
|
||||
+ if kind != "dangling":
|
||||
+ target.mkdir()
|
||||
+ if kind == "junction":
|
||||
+ if not IS_WIN:
|
||||
+ pytest.skip("requires Windows junctions")
|
||||
+ subprocess.run(
|
||||
+ ["cmd", "/c", "mklink", "/J", str(session.creator.bin_dir), str(target)],
|
||||
+ capture_output=True,
|
||||
+ check=True,
|
||||
+ )
|
||||
+ else:
|
||||
+ if not fs_supports_symlink():
|
||||
+ pytest.skip("requires directory symlinks")
|
||||
+ session.creator.bin_dir.symlink_to(target, target_is_directory=True)
|
||||
+ if kind == "file":
|
||||
+ with pytest.raises(NotADirectoryError, match="environment directory is not a real directory"):
|
||||
+ session.run()
|
||||
+ else:
|
||||
+ session.run()
|
||||
+ assert session.creator.bin_dir.resolve() == session.creator.bin_dir
|
||||
+
|
||||
+
|
||||
def test_os_path_sep_not_allowed(tmp_path, capsys) -> None:
|
||||
target = str(tmp_path / f"a{os.pathsep}b")
|
||||
err = _non_success_exit_code(capsys, target)
|
||||
@@ -326,6 +414,43 @@ def test_venv_configuration_uses_selected_options(tmp_path: Path, global_package
|
||||
assert {key: session.creator.pyenv_cfg.content.get(key) for key in expected} == expected
|
||||
|
||||
|
||||
+@pytest.mark.skipif(not CURRENT.has_venv or not fs_supports_symlink(), reason="requires venv and symlinks")
|
||||
+@pytest.mark.skipif(
|
||||
+ is_mac_os_framework(CURRENT) or is_macos_brew(CURRENT) or (IS_GRAALPY and not IS_WIN),
|
||||
+ reason="stdlib creator requires symlinks on this runtime",
|
||||
+)
|
||||
+def test_venv_recreate_with_copied_interpreter(tmp_path: Path) -> None:
|
||||
+ args: Final = [str(tmp_path / "env"), "--creator", "venv", "--no-seed"]
|
||||
+ cli_run([*args, "--symlinks"])
|
||||
+ session: Final = cli_run([*args, "--copies"])
|
||||
+ assert (session.creator.exe.is_file(), session.creator.exe.resolve().parent) == (
|
||||
+ True,
|
||||
+ session.creator.bin_dir.resolve(),
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+@pytest.mark.skipif(not CURRENT.has_venv, reason="requires interpreter with venv")
|
||||
+@pytest.mark.parametrize("mode", [pytest.param("--copies", id="copies"), pytest.param("--symlinks", id="symlinks")])
|
||||
+def test_venv_replaces_hard_linked_files(tmp_path: Path, mode: str) -> None:
|
||||
+ if mode == "--symlinks" and not fs_supports_symlink():
|
||||
+ pytest.skip("requires symlinks")
|
||||
+ if mode == "--copies" and (is_mac_os_framework(CURRENT) or is_macos_brew(CURRENT) or (IS_GRAALPY and not IS_WIN)):
|
||||
+ pytest.skip("stdlib creator requires symlinks on this runtime")
|
||||
+ session: Final = session_via_cli([str(tmp_path / "env"), "--creator", "venv", "--no-seed", "--activators=", mode])
|
||||
+ session.creator.bin_dir.mkdir(parents=True)
|
||||
+ names: Final = (
|
||||
+ ("python.exe", "pythonw.exe") if IS_WIN else ("python", "python3", f"python3.{CURRENT.version_info.minor}")
|
||||
+ )
|
||||
+ planted: Final = [session.creator.dest / "pyvenv.cfg", *(session.creator.bin_dir / name for name in names)]
|
||||
+ for path in planted:
|
||||
+ (tmp_path / f"{path.name}.original").write_bytes(b"original")
|
||||
+ os.link(tmp_path / f"{path.name}.original", path)
|
||||
+ session.run()
|
||||
+ assert [((tmp_path / f"{path.name}.original").read_bytes(), path.lstat().st_nlink) for path in planted] == [
|
||||
+ (b"original", 1)
|
||||
+ ] * len(planted)
|
||||
+
|
||||
+
|
||||
@pytest.mark.skipif(not CURRENT.has_venv, reason="requires interpreter with venv")
|
||||
def test_venv_fails_not_inline(tmp_path, capsys, mocker) -> None:
|
||||
if hasattr(os, "geteuid") and os.geteuid() == 0:
|
||||
diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py
|
||||
index ea7030b..c4c448a 100644
|
||||
--- a/tests/unit/test_util.py
|
||||
+++ b/tests/unit/test_util.py
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
|
||||
from virtualenv.app_data import _cache_dir_with_migration, _default_app_data_dir
|
||||
from virtualenv.util.lock import ReentrantFileLock
|
||||
-from virtualenv.util.path import copy
|
||||
+from virtualenv.util.path import copy, write_file
|
||||
from virtualenv.util.subprocess import run_cmd
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -60,6 +60,15 @@ def test_copy_replaces_contents(tmp_path: Path, directory: bool) -> None:
|
||||
assert (destination / "entry" if directory else destination).read_bytes() == b"new"
|
||||
|
||||
|
||||
+@pytest.mark.parametrize("existing", [pytest.param(False, id="new"), pytest.param(True, id="replace")])
|
||||
+def test_write_file_replaces_content(tmp_path: Path, existing: bool) -> None:
|
||||
+ path: Final = tmp_path / "generated"
|
||||
+ if existing:
|
||||
+ path.write_bytes(b"previous")
|
||||
+ write_file(path, b"generated")
|
||||
+ assert path.read_bytes() == b"generated"
|
||||
+
|
||||
+
|
||||
def test_run_fail(tmp_path) -> None:
|
||||
code, out, err = run_cmd([str(tmp_path)])
|
||||
assert err
|
||||
--
|
||||
2.35.6
|
||||
@@ -26,6 +26,7 @@ SRC_URI += " \
|
||||
file://CVE-2026-113715.patch \
|
||||
file://CVE-2026-113720.patch \
|
||||
file://CVE-2026-113719.patch \
|
||||
file://CVE-2026-113721.patch \
|
||||
"
|
||||
|
||||
BBCLASSEXTEND = "native nativesdk"
|
||||
|
||||
Reference in new issue
Block a user