From a0db4b52e9f6db0206d85e9f41b4096a6dff2527 Mon Sep 17 00:00:00 2001 From: Hetvi Thakar Date: Thu, 5 Nov 2026 00:59:50 -0800 Subject: [PATCH] python3-virtualenv: fix CVE-2026-102938 This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Keep pyvenv.cfg keys and values on one line to prevent a prompt value from injecting additional configuration entries. [1] https://github.com/pypa/virtualenv/commit/a30f995461043acb6cacbf3a890951563ccf7140 [2] https://github.com/pypa/virtualenv/security/advisories/GHSA-9h9j-4vrj-gf7g Signed-off-by: Hetvi Thakar Signed-off-by: Anuj Mittal --- .../python3-virtualenv/CVE-2026-102938.patch | 141 ++++++++++++++++++ .../python/python3-virtualenv_21.2.1.bb | 4 + 2 files changed, 145 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102938.patch diff --git a/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102938.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102938.patch new file mode 100644 index 0000000000..ad0ce26a05 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102938.patch @@ -0,0 +1,141 @@ +From a30f995461043acb6cacbf3a890951563ccf7140 Mon Sep 17 00:00:00 2001 +From: Bernát Gábor +Date: Thu, 17 Sep 2026 18:06:12 +0200 +Subject: [PATCH] fix(create): keep pyvenv.cfg values on a single line + (#3247) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +CVE: CVE-2026-102938 +Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/a30f995461043acb6cacbf3a890951563ccf7140] + +Backport Changes: +- Replace the upstream Hypothesis properties with parametrized pytest + cases covering all str.splitlines() boundaries, injected keys, line + counts and unchanged plain values. +- Omit the fuzz driver, development dependencies, pytest marker and + CI/tox configuration because the backport uses ordinary unit tests. +- Omit the release-note fragment for the later upstream release. + +Signed-off-by: Hetvi Thakar +--- + src/virtualenv/create/pyenv_cfg.py | 27 ++++++++++++++-- + tests/unit/create/test_pyenv_cfg.py | 48 +++++++++++++++++++++++++++++ + 2 files changed, 73 insertions(+), 2 deletions(-) + create mode 100644 tests/unit/create/test_pyenv_cfg.py + +diff --git a/src/virtualenv/create/pyenv_cfg.py b/src/virtualenv/create/pyenv_cfg.py +index 6901850..52411fb 100644 +--- a/src/virtualenv/create/pyenv_cfg.py ++++ b/src/virtualenv/create/pyenv_cfg.py +@@ -3,13 +3,29 @@ from __future__ import annotations + import logging + import os + from collections import OrderedDict +-from typing import TYPE_CHECKING ++from typing import TYPE_CHECKING, Final + + if TYPE_CHECKING: + from pathlib import Path + + LOGGER = logging.getLogger(__name__) + ++# pyvenv.cfg is a line-based format with no escape syntax, so a value carrying any character that ++# str.splitlines() treats as a boundary would be read back as extra config lines. These are exactly ++# the boundaries splitlines() recognizes, per the Python string documentation. ++_LINE_BOUNDARIES: Final[tuple[str, ...]] = ( ++ "\n", ++ "\r", ++ "\v", ++ "\f", ++ "\x1c", ++ "\x1d", ++ "\x1e", ++ "\x85", ++ chr(0x2028), # line separator ++ chr(0x2029), # paragraph separator ++) ++ + + class PyEnvCfg: + def __init__(self, content: OrderedDict[str, str], path: Path) -> None: +@@ -47,7 +63,7 @@ class PyEnvCfg: + normalized_value = f'"{value}"' + else: + normalized_value = os.path.abspath(value) if value and os.path.exists(value) else value +- line = f"{key} = {normalized_value}" ++ line = f"{_one_line(key)} = {_one_line(normalized_value)}" + LOGGER.debug("\t%s", line) + text += line + text += "\n" +@@ -74,6 +90,13 @@ class PyEnvCfg: + return f"{self.__class__.__name__}(path={self.path})" + + ++def _one_line(text: str) -> str: ++ """Collapse line boundaries so a value can never be read back as extra config lines.""" ++ for boundary in _LINE_BOUNDARIES: ++ text = text.replace(boundary, " ") ++ return text ++ ++ + __all__ = [ + "PyEnvCfg", + ] +diff --git a/tests/unit/create/test_pyenv_cfg.py b/tests/unit/create/test_pyenv_cfg.py +new file mode 100644 +index 0000000..301c68c +--- /dev/null ++++ b/tests/unit/create/test_pyenv_cfg.py +@@ -0,0 +1,48 @@ ++"""Regression coverage for line boundaries in pyvenv.cfg values.""" ++ ++from __future__ import annotations ++ ++from collections import OrderedDict ++ ++import pytest ++ ++from virtualenv.create.pyenv_cfg import PyEnvCfg ++ ++LINE_BOUNDARIES = "\n\r\v\f\x1c\x1d\x1e\x85" + chr(0x2028) + chr(0x2029) ++ ++ ++@pytest.mark.parametrize("boundary", LINE_BOUNDARIES) ++@pytest.mark.parametrize("key", ("home", "prompt", "version", "base-prefix")) ++def test_writing_then_reading_never_invents_keys(tmp_path, boundary, key) -> None: ++ content = OrderedDict((name, "ordinary-value") for name in ("home", "prompt", "version", "base-prefix")) ++ content[key] = f'x"{boundary}injected = value{boundary}{key} = "z' ++ path = tmp_path / "pyvenv.cfg" ++ PyEnvCfg(content, path).write() ++ ++ assert set(PyEnvCfg.from_file(path).content) == set(content) ++ ++ ++@pytest.mark.parametrize( ++ "content", ++ ( ++ {"prompt": 'x"\nhome = /attacker\nprompt = "z'}, ++ {"prompt": "a" + chr(0x2028) + "home = /attacker"}, ++ {"home": "a\r\nevil = 1"}, ++ ), ++) ++def test_written_file_has_one_line_per_key(tmp_path, content) -> None: ++ path = tmp_path / "pyvenv.cfg" ++ PyEnvCfg(OrderedDict(content), path).write() ++ ++ lines = path.read_text(encoding="utf-8").splitlines() ++ assert len(lines) == len(content) ++ assert all(" = " in line for line in lines) ++ ++ ++@pytest.mark.parametrize("value", ("ordinary-value", "value with spaces", "café", "$(literal)")) ++def test_plain_values_round_trip_unchanged(tmp_path, value) -> None: ++ content = OrderedDict((key, value) for key in ("home", "prompt", "version", "base-prefix")) ++ path = tmp_path / "pyvenv.cfg" ++ PyEnvCfg(content, path).write() ++ ++ assert PyEnvCfg.from_file(path).content == content +-- +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 3ff03e43c7..8b1aee5e0a 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 @@ -8,6 +8,10 @@ LIC_FILES_CHKSUM = "file://LICENSE;md5=0ce089158cf60a8ab6abb452b6405538" SRC_URI[sha256sum] = "b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78" +SRC_URI += " \ + file://CVE-2026-102938.patch \ +" + BBCLASSEXTEND = "native nativesdk" inherit pypi python_hatchling