diff --git a/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p1.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p1.patch new file mode 100644 index 0000000000..1afedad035 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p1.patch @@ -0,0 +1,116 @@ +From 3bddeaf8d02c3ee7f576503efb082f8f494cfcab Mon Sep 17 00:00:00 2001 +From: Bernát Gábor +Date: Wed, 30 Sep 2026 22:59:45 -0700 +Subject: [PATCH] Merge commit from fork + +FishActivator inherited shlex.quote, whose POSIX rules leave a backslash +literal inside single quotes. fish reads \' there as an escaped quote, so a +path or --prompt with a backslash before a quote ended the literal early and +ran the rest as commands, without any error from fish. + +Give FishActivator its own quote() that escapes backslashes and quotes. + +CVE: CVE-2026-113732 +Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/3bddeaf8d02c3ee7f576503efb082f8f494cfcab] + +Backport Changes: +- Add the cli_run import required by the upstream regression test; + it is absent from the 21.2.1 test module. + +Signed-off-by: Hetvi Thakar +--- + src/virtualenv/activation/fish/__init__.py | 6 ++++ + tests/unit/activation/test_fish.py | 40 ++++++++++++++++++++-- + 2 files changed, 43 insertions(+), 3 deletions(-) + +diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py +index 74300db..81d6d92 100644 +--- a/src/virtualenv/activation/fish/__init__.py ++++ b/src/virtualenv/activation/fish/__init__.py +@@ -15,6 +15,12 @@ class FishActivator(ViaTemplateActivator): + def templates(self) -> Iterator[str]: + yield "activate.fish" + ++ @staticmethod ++ def quote(string: str) -> str: ++ # fish reads \\ and \' as escapes inside single quotes, unlike POSIX sh, so shlex.quote's output lets a ++ # backslash before a quote end the literal early ++ return "'" + string.replace("\\", "\\\\").replace("'", "\\'") + "'" ++ + def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]: + data = super().replacements(creator, dest_folder) + data.update({ +diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py +index f726e7e..fc1b8a6 100644 +--- a/tests/unit/activation/test_fish.py ++++ b/tests/unit/activation/test_fish.py +@@ -1,7 +1,6 @@ + from __future__ import annotations + + import os +-import shlex + import shutil + import subprocess + import sys +@@ -13,6 +12,7 @@ import pytest + + from virtualenv.activation import FishActivator + from virtualenv.info import IS_WIN ++from virtualenv.run import cli_run + + if TYPE_CHECKING: + from collections.abc import Callable +@@ -59,8 +59,8 @@ def test_fish_tkinter_generation( + assert "set -e _OLD_PKG_CONFIG_PATH" in content + + if present: +- assert f"set -gx TCL_LIBRARY {shlex.quote(tcl_lib)}\n" in content +- assert f"set -gx TK_LIBRARY {shlex.quote(tk_lib)}\n" in content ++ assert f"set -gx TCL_LIBRARY {FishActivator.quote(tcl_lib)}\n" in content ++ assert f"set -gx TK_LIBRARY {FishActivator.quote(tk_lib)}\n" in content + else: + assert "if test -n ''\n if set -q TCL_LIBRARY;" in content + assert "if test -n ''\n if set -q TK_LIBRARY;" in content +@@ -79,6 +79,40 @@ def test_fish_tkinter_path_does_not_run_commands( + assert not marker.exists() + + ++@pytest.mark.skipif(IS_WIN, reason="fish is not available on Windows") ++@pytest.mark.skipif(FISH is None, reason="fish is not installed") ++@pytest.mark.parametrize( ++ ("prompt", "env_name"), ++ [ ++ pytest.param(r"x\'$(touch PWNED)'\'", "env", id="prompt-backslash-quote"), ++ pytest.param(None, r"x\'$(touch PWNED)'\'", id="dirname-backslash-quote"), ++ ], ++) ++def test_fish_activate_does_not_run_commands( ++ tmp_path: Path, current_fastest: str, prompt: str | None, env_name: str ++) -> None: ++ dest = tmp_path / env_name ++ args = ["--without-pip", str(dest), "--creator", current_fastest, "--no-periodic-update", "--activators", "fish"] ++ if prompt is not None: ++ args += ["--prompt", prompt] ++ cli_run(args) ++ (work_dir := tmp_path / "workdir").mkdir() ++ ++ # the path is passed as an argument so the driver cannot expand a payload dir name while locating the script ++ result = subprocess.run( ++ [FISH, "--no-config", "-c", 'source $argv[1]; printf "%s" "$VIRTUAL_ENV"', str(dest / "bin" / "activate.fish")], ++ capture_output=True, ++ text=True, ++ encoding="utf-8", ++ cwd=str(work_dir), ++ timeout=60, ++ check=False, ++ ) ++ ++ assert not (work_dir / "PWNED").exists() ++ assert result.stdout == str(dest), result.stderr ++ ++ + @pytest.mark.skipif(IS_WIN, reason="we have not setup fish in CI yet") + def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path) -> None: + monkeypatch.setenv("HOME", str(tmp_path)) +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p2.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p2.patch new file mode 100644 index 0000000000..ad66d9b1e2 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p2.patch @@ -0,0 +1,120 @@ +From 685435d7fb53da12da99e195ee2753f5ca083249 Mon Sep 17 00:00:00 2001 +From: Bernát Gábor +Date: Sat, 17 Oct 2026 02:08:22 -0700 +Subject: [PATCH] 🐛 fix(fish): separate escapes from multibyte text (#3413) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +CVE: CVE-2026-113732 +Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/685435d7fb53da12da99e195ee2753f5ca083249] + +Backport Changes: +- Omit the release-only changelog fragment; the release documentation + directory is absent from the 21.2.1 source distribution. + +Signed-off-by: Hetvi Thakar +--- + src/virtualenv/activation/fish/__init__.py | 6 +-- + tests/unit/activation/test_fish.py | 56 +++++++++++++++++++++- + 2 files changed, 58 insertions(+), 4 deletions(-) + +diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py +index 81d6d92..7f376a9 100644 +--- a/src/virtualenv/activation/fish/__init__.py ++++ b/src/virtualenv/activation/fish/__init__.py +@@ -1,5 +1,6 @@ + from __future__ import annotations + ++import re + from typing import TYPE_CHECKING + + from virtualenv.activation.via_template import ViaTemplateActivator +@@ -17,9 +18,8 @@ class FishActivator(ViaTemplateActivator): + + @staticmethod + def quote(string: str) -> str: +- # fish reads \\ and \' as escapes inside single quotes, unlike POSIX sh, so shlex.quote's output lets a +- # backslash before a quote end the literal early +- return "'" + string.replace("\\", "\\\\").replace("'", "\\'") + "'" ++ # Keep escapes beside ASCII quotes so legacy decoders cannot absorb them into a preceding multibyte character. ++ return "'" + re.sub(r"[\\']", lambda match: f"'\\{match.group()}'", string) + "'" + + def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]: + data = super().replacements(creator, dest_folder) +diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py +index fc1b8a6..d131043 100644 +--- a/tests/unit/activation/test_fish.py ++++ b/tests/unit/activation/test_fish.py +@@ -6,7 +6,7 @@ import subprocess + import sys + from argparse import Namespace + from types import SimpleNamespace +-from typing import TYPE_CHECKING ++from typing import TYPE_CHECKING, Final + + import pytest + +@@ -21,6 +21,60 @@ if TYPE_CHECKING: + FISH = shutil.which("fish") + + ++@pytest.mark.skipif(IS_WIN or FISH is None, reason="fish is required") ++@pytest.mark.parametrize( ++ "value", ++ [ ++ pytest.param("", id="empty"), ++ pytest.param("plain text", id="space"), ++ pytest.param("café's", id="unicode-apostrophe"), ++ pytest.param("café\\path", id="unicode-backslash"), ++ ], ++) ++def test_fish_quote_preserves_literal(value: str) -> None: ++ result: Final = subprocess.run( ++ [FISH, "--no-config"], ++ input=f"printf '%s' {FishActivator.quote(value)}\n", ++ capture_output=True, ++ text=True, ++ encoding="utf-8", ++ timeout=30, ++ check=True, ++ ) ++ assert result.stdout == value ++ ++ ++@pytest.mark.skipif(IS_WIN or FISH is None, reason="fish is required") ++@pytest.mark.parametrize("value", [pytest.param("中's", id="apostrophe"), pytest.param("中\\path", id="backslash")]) ++def test_fish_quote_preserves_legacy_locale_bytes(value: str) -> None: ++ version: Final = subprocess.check_output([FISH, "--version"], text=True, encoding="utf-8") ++ if not version.startswith("fish, version 3."): ++ pytest.skip("fish 4 always decodes UTF-8") ++ locales: Final = subprocess.check_output(["locale", "-a"], text=True, encoding="utf-8").splitlines() ++ if "zh_CN.gbk" not in locales: ++ pytest.skip("GBK locale is unavailable") ++ result: Final = subprocess.run( ++ [FISH, "--no-config"], ++ input=f"printf '%s' {FishActivator.quote(value)}\n".encode(), ++ env={**os.environ, "LC_ALL": "zh_CN.gbk"}, ++ capture_output=True, ++ timeout=30, ++ check=True, ++ ) ++ assert result.stdout == value.encode() ++ ++ ++@pytest.mark.parametrize( ++ ("value", "expected"), ++ [ ++ pytest.param("café's", "'café'\\''s'", id="apostrophe"), ++ pytest.param("café\\path", "'café'\\\\'path'", id="backslash"), ++ ], ++) ++def test_fish_quote_separates_escapes(value: str, expected: str) -> None: ++ assert FishActivator.quote(value) == expected ++ ++ + @pytest.fixture + def rendered_activate_fish(tmp_path: Path) -> Callable[[str | None, str | None], Path]: + def render(tcl_lib: str | None, tk_lib: str | None) -> Path: +-- +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 756a7842b8..66354956b9 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 @@ -15,6 +15,8 @@ SRC_URI += " \ file://CVE-2026-102930_p2.patch \ file://CVE-2026-102930_p3.patch \ file://CVE-2026-102925.patch \ + file://CVE-2026-113732_p1.patch \ + file://CVE-2026-113732_p2.patch \ " BBCLASSEXTEND = "native nativesdk"