mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-13 03:37:15 +00:00
python3-virtualenv: fix CVE-2026-113722
This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Preserve interpreter paths as literal data in POSIX console launchers for both the app-data and pip seeders, and update the pip launchers' RECORD hashes after rewriting them. Include the RECORD-writing follow-up from [3] so the new rewrite path replaces existing links and uses exclusive creation. The required write_file helper comes from the preceding CVE-2026-113721 fix [4]. Regenerate existing environments to repair their launchers. A later independent pip install or upgrade can replace the corrected launchers; this backport does not modify pip's vendored distlib. [1] https://github.com/pypa/virtualenv/commit/2519941bed33ee0d3ac99277f67913e439e476a9 [2] https://nvd.nist.gov/vuln/detail/CVE-2026-113722 [3] https://github.com/pypa/virtualenv/commit/3598e201f6f044e389136b2347a8a919ac03c5f4 [4] https://github.com/pypa/virtualenv/commit/d25f28699eefb1fc34a251e5adf2b5de844c36cd 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
d54f2315dd
commit
021ed63fde
3 files changed
+314
No files matched your search
@@ -0,0 +1,262 @@
|
||||
From 2519941bed33ee0d3ac99277f67913e439e476a9 Mon Sep 17 00:00:00 2001
|
||||
From: Bernát Gábor <gaborjbernat@gmail.com>
|
||||
Date: Sat, 17 Oct 2026 03:41:16 -0700
|
||||
Subject: [PATCH] fix(seed): preserve interpreter paths in console scripts
|
||||
(#3417)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
CVE: CVE-2026-113722
|
||||
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/2519941bed33ee0d3ac99277f67913e439e476a9]
|
||||
|
||||
Backport Changes:
|
||||
- Add the os import absent from the older pip seeder and import only
|
||||
shell_shebang, without the later EntryPointConfig helper. The older
|
||||
module has no existing EntryPointConfig import to replace.
|
||||
- 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/seed/embed/pip_invoke.py | 44 +++++++++
|
||||
.../embed/via_app_data/pip_install/base.py | 15 +++
|
||||
.../embed/test_bootstrap_link_via_app_data.py | 92 ++++++++++++++++++-
|
||||
3 files changed, 150 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/virtualenv/seed/embed/pip_invoke.py b/src/virtualenv/seed/embed/pip_invoke.py
|
||||
index 1f333d3..6a16605 100644
|
||||
--- a/src/virtualenv/seed/embed/pip_invoke.py
|
||||
+++ b/src/virtualenv/seed/embed/pip_invoke.py
|
||||
@@ -1,14 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
+import csv
|
||||
+import hashlib
|
||||
+import io
|
||||
import logging
|
||||
+import os
|
||||
+from base64 import urlsafe_b64encode
|
||||
from contextlib import contextmanager
|
||||
+from importlib.metadata import distributions
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
+from stat import S_IMODE
|
||||
from subprocess import Popen
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from virtualenv.seed.embed.base_embed import BaseEmbed
|
||||
+from virtualenv.seed.embed.via_app_data.pip_install.base import shell_shebang
|
||||
from virtualenv.seed.wheels import get_wheel, pip_wheel_env_run
|
||||
from virtualenv.seed.wheels.util import Version
|
||||
from virtualenv.util.subprocess import LogCmd
|
||||
@@ -33,6 +41,42 @@ class PipInvoke(BaseEmbed):
|
||||
with self.get_pip_install_cmd(creator.exe, for_py_version) as cmd:
|
||||
env = pip_wheel_env_run(self.extra_search_dir, self.app_data, self.env)
|
||||
self._execute(cmd, env)
|
||||
+ if os.name == "posix":
|
||||
+ self._rewrite_launchers(creator)
|
||||
+
|
||||
+ def _rewrite_launchers(self, creator: Creator) -> None:
|
||||
+ selected: Final = self.distribution_to_versions()
|
||||
+ exe: Final = str(creator.exe).encode("utf-8")
|
||||
+ for distribution in distributions(path=sorted({str(creator.purelib), str(creator.platlib)})):
|
||||
+ if distribution.metadata["Name"].lower().replace("-", "_") not in selected:
|
||||
+ continue
|
||||
+ rows: Final = list(csv.reader(io.StringIO(distribution.read_text("RECORD") or "")))
|
||||
+ changed = False
|
||||
+ for row in rows:
|
||||
+ path: Final = Path(os.path.abspath(str(distribution.locate_file(row[0]))))
|
||||
+ if path.parent != creator.script_dir or not path.is_file():
|
||||
+ continue
|
||||
+ content = path.read_bytes()
|
||||
+ if content.startswith(b"#!/bin/sh\n'''exec' "):
|
||||
+ body = content.split(b"\n", 3)[3]
|
||||
+ elif content.startswith(b"#!" + exe + b"\n") and len(exe.split()) > 1:
|
||||
+ body = content.split(b"\n", 1)[1]
|
||||
+ else:
|
||||
+ continue
|
||||
+ content = shell_shebang(str(creator.exe)) + body
|
||||
+ mode: Final = S_IMODE(path.stat().st_mode) & 0o755
|
||||
+ path.unlink()
|
||||
+ with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode), "wb") as stream:
|
||||
+ stream.write(content)
|
||||
+ row[1:] = [
|
||||
+ "sha256=" + urlsafe_b64encode(hashlib.sha256(content).digest()).rstrip(b"=").decode("ascii"),
|
||||
+ str(len(content)),
|
||||
+ ]
|
||||
+ changed = True
|
||||
+ if changed:
|
||||
+ record: Final = next(row[0] for row in rows if row[0].endswith(".dist-info/RECORD"))
|
||||
+ with Path(str(distribution.locate_file(record))).open("w", encoding="utf-8", newline="") as stream:
|
||||
+ csv.writer(stream).writerows(rows)
|
||||
|
||||
@staticmethod
|
||||
def _execute(cmd: list[str], env: dict[str, str]) -> Popen[bytes]:
|
||||
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 086ae0c..c741bd3 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
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
+import shlex
|
||||
import zipfile
|
||||
from abc import ABC, abstractmethod
|
||||
from configparser import ConfigParser
|
||||
@@ -214,6 +215,13 @@ class ScriptMakerCustom(ScriptMaker):
|
||||
self.version_info = version_info.major, version_info.minor # ty: ignore[unresolved-attribute]
|
||||
self.variants = {"", "X", "X.Y"}
|
||||
self._name = name
|
||||
+ self._raw_executable = str(executable)
|
||||
+
|
||||
+ def _build_shebang(self, executable: bytes, post_interp: bytes) -> bytes:
|
||||
+ shebang: Final = super()._build_shebang(executable, post_interp)
|
||||
+ if os.name != "posix" or len(shebang.split()) == 1:
|
||||
+ return shebang
|
||||
+ return shell_shebang(self._raw_executable, post_interp)
|
||||
|
||||
def _write_script(
|
||||
self, names: set[str], shebang: bytes, script_bytes: bytes, filenames: list[str], ext: str
|
||||
@@ -226,6 +234,13 @@ class ScriptMakerCustom(ScriptMaker):
|
||||
super()._write_script(names, shebang, script_bytes, filenames, ext)
|
||||
|
||||
|
||||
+def shell_shebang(executable: str, post_interp: bytes = b"") -> bytes:
|
||||
+ # The exec line is read by both sh and Python; double-quoted backslashes survive both readers.
|
||||
+ quoted: Final = shlex.quote(executable).replace("\\", "'\"\\\\\"'")
|
||||
+ return b"#!/bin/sh\n'''exec' " + quoted.encode("utf-8") + post_interp + b' "$0" "$@"\n' + b"' '''\n"
|
||||
+
|
||||
+
|
||||
__all__ = [
|
||||
"PipInstall",
|
||||
+ "shell_shebang",
|
||||
]
|
||||
diff --git a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py
|
||||
index 278a48e..a9917c4 100644
|
||||
--- a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py
|
||||
+++ b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
+import csv
|
||||
+import hashlib
|
||||
import os
|
||||
import sys
|
||||
+from base64 import urlsafe_b64encode
|
||||
from stat import S_IWGRP, S_IWOTH, S_IWUSR
|
||||
-from subprocess import Popen, check_call
|
||||
+from subprocess import Popen, check_call, run
|
||||
from threading import Thread
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
@@ -23,6 +26,7 @@ if TYPE_CHECKING:
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
|
||||
+@pytest.mark.usefixtures("_no_coverage")
|
||||
@pytest.mark.parametrize(
|
||||
("metadata_file", "content"),
|
||||
[
|
||||
@@ -109,6 +113,70 @@ def test_seed_metadata_keeps_reseeding_supported(
|
||||
)
|
||||
|
||||
|
||||
+@pytest.mark.parametrize(
|
||||
+ "name",
|
||||
+ [
|
||||
+ pytest.param("plain", id="plain"),
|
||||
+ pytest.param("two words", id="space"),
|
||||
+ pytest.param("owner's env", id="apostrophe"),
|
||||
+ pytest.param("$(echo a) `echo b`", id="command-substitution"),
|
||||
+ pytest.param("a" * 120, id="long", marks=pytest.mark.skipif(os.name == "nt", reason="POSIX shebang limit")),
|
||||
+ pytest.param("env \\path", id="backslash", marks=pytest.mark.skipif(os.name == "nt", reason="POSIX filename")),
|
||||
+ pytest.param("env\tname", id="tab", marks=pytest.mark.skipif(os.name == "nt", reason="POSIX filename")),
|
||||
+ ],
|
||||
+)
|
||||
+@pytest.mark.parametrize(
|
||||
+ "via_python",
|
||||
+ [
|
||||
+ pytest.param(False, id="launcher"),
|
||||
+ pytest.param(
|
||||
+ True, id="python", marks=pytest.mark.skipif(os.name == "nt", reason="Windows launcher is an executable")
|
||||
+ ),
|
||||
+ ],
|
||||
+)
|
||||
+@pytest.mark.parametrize("seeder", [pytest.param("app-data", id="app-data"), pytest.param("pip", id="pip")])
|
||||
+def test_console_script_preserves_interpreter_path(
|
||||
+ tmp_path: Path, current_fastest: str, name: str, via_python: bool, seeder: str
|
||||
+) -> None:
|
||||
+ destination: Final = os.path.join(str(tmp_path), name, "env")
|
||||
+ run(
|
||||
+ [
|
||||
+ sys.executable,
|
||||
+ "-m",
|
||||
+ "virtualenv",
|
||||
+ destination,
|
||||
+ "--seeder",
|
||||
+ seeder,
|
||||
+ "--creator",
|
||||
+ current_fastest,
|
||||
+ "--activators",
|
||||
+ "",
|
||||
+ "--no-periodic-update",
|
||||
+ "--pip",
|
||||
+ "embed",
|
||||
+ ],
|
||||
+ capture_output=True,
|
||||
+ text=True,
|
||||
+ encoding="utf-8",
|
||||
+ timeout=60,
|
||||
+ check=True,
|
||||
+ )
|
||||
+ scripts: Final = os.path.join(destination, "Scripts" if os.name == "nt" else "bin")
|
||||
+ result: Final = run(
|
||||
+ [
|
||||
+ *([os.path.join(scripts, "python")] if via_python else []),
|
||||
+ os.path.join(scripts, "pip.exe" if os.name == "nt" else "pip"),
|
||||
+ "--version",
|
||||
+ ],
|
||||
+ capture_output=True,
|
||||
+ text=True,
|
||||
+ encoding="utf-8",
|
||||
+ timeout=30,
|
||||
+ check=True,
|
||||
+ )
|
||||
+ assert os.path.normcase(destination) in os.path.normcase(result.stdout)
|
||||
+
|
||||
+
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.parametrize("copies", [False, True] if fs_supports_symlink() else [True])
|
||||
def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies, for_py_version) -> None: # noqa: PLR0915
|
||||
@@ -348,3 +416,25 @@ def _run_parallel_threads(tmp_path):
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
return exceptions
|
||||
+
|
||||
+
|
||||
+@pytest.mark.skipif(os.name != "posix", reason="POSIX shell launchers")
|
||||
+@pytest.mark.parametrize(
|
||||
+ ("name", "via_shell"),
|
||||
+ [
|
||||
+ pytest.param("plain", False, id="plain"),
|
||||
+ pytest.param("two words", True, id="space"),
|
||||
+ pytest.param("env\tname", True, id="tab"),
|
||||
+ ],
|
||||
+)
|
||||
+def test_pip_launcher_record_matches_contents(tmp_path: Path, name: str, via_shell: bool) -> None:
|
||||
+ session: Final = cli_run([str(tmp_path / name), "--seeder", "pip", "--pip", "embed", "--no-periodic-update"])
|
||||
+ record: Final = next(session.creator.purelib.glob("pip-*.dist-info/RECORD"))
|
||||
+ with record.open(encoding="utf-8", newline="") as stream:
|
||||
+ row: Final = next(row for row in csv.reader(stream) if row[0].endswith("/bin/pip"))
|
||||
+ content: Final = (session.creator.script_dir / "pip").read_bytes()
|
||||
+ assert content.startswith(b"#!/bin/sh\n") is via_shell
|
||||
+ assert row[1:] == [
|
||||
+ "sha256=" + urlsafe_b64encode(hashlib.sha256(content).digest()).rstrip(b"=").decode("ascii"),
|
||||
+ str(len(content)),
|
||||
+ ]
|
||||
--
|
||||
2.35.6
|
||||
@@ -0,0 +1,50 @@
|
||||
From 3598e201f6f044e389136b2347a8a919ac03c5f4 Mon Sep 17 00:00:00 2001
|
||||
From: Bernát Gábor <gaborjbernat@gmail.com>
|
||||
Date: Thu, 22 Oct 2026 13:06:22 -0700
|
||||
Subject: [PATCH] fix(bash): escape the prompt for every PS1 state (#3466)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
CVE: CVE-2026-113722
|
||||
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/3598e201f6f044e389136b2347a8a919ac03c5f4]
|
||||
|
||||
Backport Changes:
|
||||
- Carry only pip_invoke.py's helper import and exclusive RECORD-write
|
||||
change, now required by the backported launcher rewrite. Use the
|
||||
write_file helper introduced by the CVE-2026-113721 backport.
|
||||
- Omit the remaining shell, Windows, test, tooling and documentation
|
||||
changes. The required Bash/Zsh fixes are already carried separately
|
||||
in CVE-2026-115586.patch; the other changes are outside this fix.
|
||||
|
||||
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||
---
|
||||
src/virtualenv/seed/embed/pip_invoke.py | 6 ++++--
|
||||
1 file changed, 4 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/virtualenv/seed/embed/pip_invoke.py b/src/virtualenv/seed/embed/pip_invoke.py
|
||||
index 6a16605..e098128 100644
|
||||
--- a/src/virtualenv/seed/embed/pip_invoke.py
|
||||
+++ b/src/virtualenv/seed/embed/pip_invoke.py
|
||||
@@ -19,6 +19,7 @@ from virtualenv.seed.embed.base_embed import BaseEmbed
|
||||
from virtualenv.seed.embed.via_app_data.pip_install.base import shell_shebang
|
||||
from virtualenv.seed.wheels import get_wheel, pip_wheel_env_run
|
||||
from virtualenv.seed.wheels.util import Version
|
||||
+from virtualenv.util.path import write_file
|
||||
from virtualenv.util.subprocess import LogCmd
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -75,8 +76,9 @@ class PipInvoke(BaseEmbed):
|
||||
changed = True
|
||||
if changed:
|
||||
record: Final = next(row[0] for row in rows if row[0].endswith(".dist-info/RECORD"))
|
||||
- with Path(str(distribution.locate_file(record))).open("w", encoding="utf-8", newline="") as stream:
|
||||
- csv.writer(stream).writerows(rows)
|
||||
+ buffer: Final = io.StringIO()
|
||||
+ csv.writer(buffer).writerows(rows)
|
||||
+ write_file(Path(str(distribution.locate_file(record))), buffer.getvalue().encode("utf-8"))
|
||||
|
||||
@staticmethod
|
||||
def _execute(cmd: list[str], env: dict[str, str]) -> Popen[bytes]:
|
||||
--
|
||||
2.35.6
|
||||
@@ -28,6 +28,8 @@ SRC_URI += " \
|
||||
file://CVE-2026-113719.patch \
|
||||
file://CVE-2026-113721.patch \
|
||||
file://CVE-2026-113716.patch \
|
||||
file://CVE-2026-113722_p1.patch \
|
||||
file://CVE-2026-113722_p2.patch \
|
||||
"
|
||||
|
||||
BBCLASSEXTEND = "native nativesdk"
|
||||
|
||||
Reference in new issue
Block a user