mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-13 15:47:23 +00:00
python3-virtualenv: fix CVE-2026-113719
This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Restrict seed-package metadata cleanup to entries inside the environment, without deleting the environment itself or directories containing the metadata. Unlink final symlinks without following their targets. [1] https://github.com/pypa/virtualenv/commit/e3cea6fa10a50fa29cf1fdddd772458fba076d5f [2] https://nvd.nist.gov/vuln/detail/CVE-2026-113719 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
f11354004d
commit
90f0e58d3d
2 files changed
+208
No files matched your search
@@ -0,0 +1,207 @@
|
||||
From e3cea6fa10a50fa29cf1fdddd772458fba076d5f Mon Sep 17 00:00:00 2001
|
||||
From: Bernát Gábor <gaborjbernat@gmail.com>
|
||||
Date: Sat, 17 Oct 2026 03:19:58 -0700
|
||||
Subject: [PATCH] fix(seed): contain metadata cleanup (#3428)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
CVE: CVE-2026-113719
|
||||
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/e3cea6fa10a50fa29cf1fdddd772458fba076d5f]
|
||||
|
||||
Backport Changes:
|
||||
- Add the Final import required by the unchanged upstream regression
|
||||
tests; the older test module does not already import it.
|
||||
- Omit the upstream release changelog fragment, which is not shipped
|
||||
in the 21.2.1 source archive.
|
||||
|
||||
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||
---
|
||||
.../embed/via_app_data/pip_install/base.py | 43 +++++----
|
||||
.../embed/test_bootstrap_link_via_app_data.py | 88 ++++++++++++++++++-
|
||||
2 files changed, 115 insertions(+), 16 deletions(-)
|
||||
|
||||
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 89348aa..7697e73 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
|
||||
@@ -9,7 +9,7 @@ from configparser import ConfigParser
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import TYPE_CHECKING, Final
|
||||
|
||||
from distlib.scripts import ScriptMaker, enquote_executable
|
||||
|
||||
@@ -154,35 +154,48 @@ class PipInstall(ABC):
|
||||
paths = (p for p in in_folders if p.stem.split("-")[0] == dist_name and p.suffix == ".dist-info" and p.is_dir())
|
||||
existing_dist = next(paths, None)
|
||||
if existing_dist is not None:
|
||||
- self._uninstall_dist(existing_dist)
|
||||
+ self._uninstall_dist(existing_dist, self._creator.dest)
|
||||
|
||||
@staticmethod
|
||||
- def _uninstall_dist(dist: Path) -> None:
|
||||
- dist_base = dist.parent
|
||||
+ def _uninstall_dist(dist: Path, env_dir: Path) -> None:
|
||||
+ dist_base: Final = dist.parent
|
||||
LOGGER.debug("uninstall existing distribution %s from %s", dist.stem, dist_base)
|
||||
|
||||
- top_txt = dist / "top_level.txt" # add top level packages at folder level
|
||||
- paths = (
|
||||
+ top_txt: Final = dist / "top_level.txt"
|
||||
+ paths: Final = (
|
||||
{dist.parent / i.strip() for i in top_txt.read_text(encoding="utf-8").splitlines()}
|
||||
if top_txt.exists()
|
||||
else set()
|
||||
)
|
||||
- paths.add(dist) # add the dist-info folder itself
|
||||
+ paths.add(dist)
|
||||
|
||||
- base_dirs, record = paths.copy(), dist / "RECORD" # collect entries in record that we did not register yet
|
||||
+ base_dirs: Final = paths.copy()
|
||||
+ record: Final = dist / "RECORD"
|
||||
for name in (
|
||||
(i.split(",")[0] for i in record.read_text(encoding="utf-8").splitlines()) if record.exists() else ()
|
||||
):
|
||||
- path = dist_base / name
|
||||
- if not any(p in base_dirs for p in path.parents): # only add if not already added as a base dir
|
||||
+ path: Final = dist_base / name
|
||||
+ if not any(p in base_dirs for p in path.parents):
|
||||
paths.add(path)
|
||||
|
||||
- for path in sorted(paths): # actually remove stuff in a stable order
|
||||
- if path.exists():
|
||||
- if path.is_dir() and not path.is_symlink():
|
||||
- safe_delete(path)
|
||||
+ root: Final = env_dir.resolve()
|
||||
+ metadata_parents: Final = set(dist.resolve().parents)
|
||||
+ for entry in sorted(paths):
|
||||
+ if entry.exists() or entry.is_symlink():
|
||||
+ linked: Final = entry.is_symlink()
|
||||
+ # Unlinking the final link leaves its target intact; parent links still affect containment.
|
||||
+ resolved: Final = entry.parent.resolve() / entry.name if linked else entry.resolve()
|
||||
+ if root not in resolved.parents or resolved in metadata_parents:
|
||||
+ LOGGER.warning(
|
||||
+ "skip removal of %s listed by %s: not inside environment %s or a parent of the metadata",
|
||||
+ entry,
|
||||
+ dist.name,
|
||||
+ root,
|
||||
+ )
|
||||
+ elif entry.is_dir() and not linked:
|
||||
+ safe_delete(entry)
|
||||
else:
|
||||
- path.unlink()
|
||||
+ entry.unlink()
|
||||
|
||||
def clear(self) -> None:
|
||||
if self._image_dir.exists():
|
||||
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 502f32a..278a48e 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
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
from stat import S_IWGRP, S_IWOTH, S_IWUSR
|
||||
from subprocess import Popen, check_call
|
||||
from threading import Thread
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import TYPE_CHECKING, Final
|
||||
|
||||
import pytest
|
||||
from python_discovery import PythonInfo
|
||||
@@ -23,6 +23,92 @@ if TYPE_CHECKING:
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
|
||||
+@pytest.mark.parametrize(
|
||||
+ ("metadata_file", "content"),
|
||||
+ [
|
||||
+ pytest.param("top_level.txt", "pip\n{dest}\n", id="top-level-env-root"),
|
||||
+ pytest.param("top_level.txt", "pip\n\n", id="top-level-blank"),
|
||||
+ pytest.param("RECORD", "{record}\n{outside},,", id="record-traversal"),
|
||||
+ ],
|
||||
+)
|
||||
+def test_seed_metadata_cleanup_stays_inside_package(
|
||||
+ tmp_path: Path, caplog: pytest.LogCaptureFixture, metadata_file: str, content: str
|
||||
+) -> None:
|
||||
+ args: Final = [str(tmp_path / "env"), "--pip", "embed", "--no-periodic-update"]
|
||||
+ session: Final = cli_run(args, setup_logging=False)
|
||||
+ purelib: Final = session.creator.purelib
|
||||
+ markers: Final = [tmp_path / "keep.txt", session.creator.dest / "keep.txt", purelib / "keep.txt"]
|
||||
+ for marker in markers:
|
||||
+ marker.write_text("keep", encoding="utf-8")
|
||||
+ dist_info: Final = next(purelib.glob("pip-*.dist-info"))
|
||||
+ (dist_info / metadata_file).write_text(
|
||||
+ content.format(
|
||||
+ dest=session.creator.dest,
|
||||
+ outside=os.path.relpath(markers[0], purelib),
|
||||
+ record=(dist_info / "RECORD").read_text(encoding="utf-8"),
|
||||
+ ),
|
||||
+ encoding="utf-8",
|
||||
+ )
|
||||
+ cli_run(args, setup_logging=False)
|
||||
+ assert (
|
||||
+ [i.exists() for i in markers],
|
||||
+ (purelib / "pip" / "__init__.py").is_file(),
|
||||
+ "skip removal of" in caplog.text,
|
||||
+ ) == (
|
||||
+ [True, True, True],
|
||||
+ True,
|
||||
+ True,
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+@pytest.mark.skipif(not fs_supports_symlink(), reason="symlinks are unavailable")
|
||||
+def test_seed_metadata_unlinks_symlink_without_following(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
||||
+ args: Final = [str(tmp_path / "env"), "--pip", "embed", "--no-periodic-update"]
|
||||
+ session: Final = cli_run(args, setup_logging=False)
|
||||
+ outside: Final = tmp_path / "outside"
|
||||
+ outside.mkdir()
|
||||
+ (outside / "keep.txt").write_text("keep", encoding="utf-8")
|
||||
+ link: Final = session.creator.purelib / "linked"
|
||||
+ link.symlink_to(outside, target_is_directory=True)
|
||||
+ record: Final = next(session.creator.purelib.glob("pip-*.dist-info")) / "RECORD"
|
||||
+ record.write_text(f"{record.read_text(encoding='utf-8')}\nlinked,,", encoding="utf-8")
|
||||
+ cli_run(args, setup_logging=False)
|
||||
+ assert (link.is_symlink(), (outside / "keep.txt").exists(), "skip removal of" in caplog.text) == (
|
||||
+ False,
|
||||
+ True,
|
||||
+ False,
|
||||
+ )
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize(
|
||||
+ "linked",
|
||||
+ [
|
||||
+ pytest.param(False, id="copy"),
|
||||
+ pytest.param(
|
||||
+ True, id="symlink", marks=pytest.mark.skipif(not fs_supports_symlink(), reason="symlinks are unavailable")
|
||||
+ ),
|
||||
+ ],
|
||||
+)
|
||||
+def test_seed_metadata_keeps_reseeding_supported(
|
||||
+ tmp_path: Path, linked: bool, caplog: pytest.LogCaptureFixture
|
||||
+) -> None:
|
||||
+ args: Final = [
|
||||
+ str(tmp_path / "env"),
|
||||
+ "--pip",
|
||||
+ "embed",
|
||||
+ "--no-periodic-update",
|
||||
+ *(["--symlink-app-data"] if linked else []),
|
||||
+ ]
|
||||
+ cli_run(args, setup_logging=False)
|
||||
+ session: Final = cli_run(args, setup_logging=False)
|
||||
+ package: Final = session.creator.purelib / "pip"
|
||||
+ assert ((package / "__init__.py").is_file(), package.is_symlink(), "skip removal of" in caplog.text) == (
|
||||
+ True,
|
||||
+ linked,
|
||||
+ False,
|
||||
+ )
|
||||
+
|
||||
+
|
||||
@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
|
||||
--
|
||||
2.35.6
|
||||
@@ -25,6 +25,7 @@ SRC_URI += " \
|
||||
file://CVE-2026-115586.patch \
|
||||
file://CVE-2026-113715.patch \
|
||||
file://CVE-2026-113720.patch \
|
||||
file://CVE-2026-113719.patch \
|
||||
"
|
||||
|
||||
BBCLASSEXTEND = "native nativesdk"
|
||||
|
||||
Reference in new issue
Block a user