python3-virtualenv: fix CVE-2026-113715

This patch applies the upstream fix as referenced in [2], using the
commit shown in [1]. Reject line boundaries in stdlib venv paths and
regenerate pyvenv.cfg from the selected interpreter and options,
instead of merging potentially injected configuration read back from
the intermediate file.

[1] https://github.com/pypa/virtualenv/commit/b0008a58885ded9227faf8f910c6c402712c750a
[2] https://github.com/pypa/virtualenv/security/advisories/GHSA-qmx5-4p3x-wvqf

Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Hetvi Thakar authored and Anuj Mittal committed 2026-11-09 09:02:16 +05:30
1 parent 8d74b1295e
commit 2d3e0c1b5d
2 files changed
+166

No files matched your search

@@ -0,0 +1,165 @@
From b0008a58885ded9227faf8f910c6c402712c750a Mon Sep 17 00:00:00 2001
From: Bernát Gábor <gaborjbernat@gmail.com>
Date: Sat, 17 Oct 2026 02:56:02 -0700
Subject: [PATCH] fix(create): reject ambiguous venv paths (#3426)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CVE: CVE-2026-113715
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/b0008a58885ded9227faf8f910c6c402712c750a]
Backport Changes:
- Reuse the identical _one_line helper already backported for
CVE-2026-102938 because 21.2.1 lacks the later
virtualenv.util.text.collapse_line_boundaries helper.
- Use the 21.2.1 system_executable API and GraalVM implementation name
in the launcher settings and regression test instead of the later
system_exe API and GraalPy name.
- Import Final in the older test module for the upstream annotations.
- Omit the upstream release changelog fragment.
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
src/virtualenv/create/via_global_ref/venv.py | 26 +++++++--
tests/unit/create/test_creator.py | 56 ++++++++++++++++++++
2 files changed, 77 insertions(+), 5 deletions(-)
diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py
index 9c95d12..2bfd1b4 100644
--- a/src/virtualenv/create/via_global_ref/venv.py
+++ b/src/virtualenv/create/via_global_ref/venv.py
@@ -1,11 +1,12 @@
from __future__ import annotations
import logging
-from copy import copy
-from typing import TYPE_CHECKING
+from argparse import ArgumentTypeError
+from typing import TYPE_CHECKING, Final
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
@@ -67,6 +68,20 @@ class Venv(ViaGlobalRefApi):
for exe in creator.executables(self.interpreter):
exe.run(creator, self.symlinks)
+ @classmethod
+ def validate_dest(cls, raw_value: str) -> str:
+ return cls._check_line_boundaries(super().validate_dest(cls._check_line_boundaries(raw_value)))
+
+ @staticmethod
+ def _check_line_boundaries(value: str) -> str:
+ # stdlib venv writes the destination into pyvenv.cfg without escaping line boundaries.
+ if collapse_line_boundaries(value) != value:
+ msg: Final = (
+ f"destination {value!r} must not contain a line boundary as venv cannot record it in pyvenv.cfg"
+ )
+ raise ArgumentTypeError(msg)
+ return value
+
def create_inline(self) -> None:
from venv import EnvBuilder # noqa: PLC0415
@@ -95,10 +110,11 @@ class Venv(ViaGlobalRefApi):
return cmd # ty: ignore[invalid-return-type]
def set_pyenv_cfg(self) -> None:
- # prefer venv options over ours, but keep our extra
- venv_content = copy(self.pyenv_cfg.refresh())
super().set_pyenv_cfg()
- self.pyenv_cfg.update(venv_content)
+ if self.interpreter.implementation == "GraalVM":
+ # GraalPy launchers need these settings on Windows and macOS.
+ self.pyenv_cfg["venvlauncher_command"] = self.interpreter.system_executable
+ self.pyenv_cfg["base-executable"] = self.interpreter.system_executable
def __getattribute__(self, item: str) -> object:
describe = object.__getattribute__(self, "describe")
diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py
index 3a8ad25..bd0f071 100644
--- a/tests/unit/create/test_creator.py
+++ b/tests/unit/create/test_creator.py
@@ -19,6 +19,7 @@ from pathlib import Path
from stat import S_IREAD, S_IRGRP, S_IROTH
from textwrap import dedent
from threading import Thread
+from typing import Final
import pytest
from python_discovery import PythonInfo
@@ -95,6 +96,42 @@ def system(session_app_data):
CURRENT_CREATORS = [i for i in CreatorSelector.for_interpreter(CURRENT).key_to_class if i != "builtin"]
+
+
+@pytest.mark.skipif("venv" not in CURRENT_CREATORS, reason="stdlib venv is unavailable")
+@pytest.mark.parametrize(
+ "boundary",
+ [
+ pytest.param(char, id=name)
+ for name, char in (
+ ("newline", "\n"),
+ ("return", "\r"),
+ ("vertical-tab", "\v"),
+ ("form-feed", "\f"),
+ ("file-separator", "\x1c"),
+ ("group-separator", "\x1d"),
+ ("record-separator", "\x1e"),
+ ("next-line", "\x85"),
+ ("line-separator", "\u2028"),
+ ("paragraph-separator", "\u2029"),
+ )
+ ],
+)
+def test_venv_creator_rejects_line_boundary(tmp_path: Path, capsys: pytest.CaptureFixture[str], boundary: str) -> None:
+ destination: Final = tmp_path / f"project{boundary}name"
+ with pytest.raises(SystemExit) as context:
+ cli_run([str(destination), "--creator", "venv", "--no-seed"])
+ assert context.value.code == 2
+ assert "must not contain a line boundary" in capsys.readouterr().err
+ assert not list(tmp_path.iterdir())
+
+
+@pytest.mark.skipif("venv" not in CURRENT_CREATORS, reason="stdlib venv is unavailable")
+def test_venv_creator_accepts_plain_destination(tmp_path: Path) -> None:
+ session: Final = cli_run([str(tmp_path / "environment"), "--creator", "venv", "--no-seed"])
+ assert (session.creator.dest / "pyvenv.cfg").is_file()
+
+
CREATE_METHODS = []
for k, v in CreatorSelector.for_interpreter(CURRENT).key_to_meta.items():
if k in CURRENT_CREATORS:
@@ -270,6 +307,25 @@ def test_create_vcs_ignore_exists_override(tmp_path) -> None:
assert git_ignore.read_text(encoding="utf-8") == "magic"
+@pytest.mark.skipif(not CURRENT.has_venv, reason="requires venv")
+@pytest.mark.parametrize("global_packages", [pytest.param(False, id="isolated"), pytest.param(True, id="global")])
+def test_venv_configuration_uses_selected_options(tmp_path: Path, global_packages: bool) -> None:
+ args: Final = [str(tmp_path / "env"), "--creator", "venv", "--no-seed"]
+ if global_packages:
+ args.append("--system-site-packages")
+ session: Final = cli_run(args)
+ session.creator.pyenv_cfg.path.write_text("vendor-marker = unused\n", encoding="utf-8")
+ session.creator.set_pyenv_cfg()
+ executable: Final = session.creator.interpreter.system_executable if CURRENT.implementation == "GraalVM" else None
+ expected: Final = {
+ "vendor-marker": None,
+ "include-system-site-packages": str(global_packages).lower(),
+ "venvlauncher_command": executable,
+ "base-executable": executable,
+ }
+ assert {key: session.creator.pyenv_cfg.content.get(key) for key in expected} == expected
+
+
@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:
--
2.35.6
@@ -23,6 +23,7 @@ SRC_URI += " \
file://CVE-2026-113725.patch \
file://CVE-2026-113723.patch \
file://CVE-2026-115586.patch \
file://CVE-2026-113715.patch \
"
BBCLASSEXTEND = "native nativesdk"