mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-13 03:37:15 +00:00
python3-virtualenv: fix CVE-2026-113720
This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Apply the current umask when copying interpreter and seed-package files instead of inheriting cached permission bits. Do not grant group or world write access to copied files. [1] https://github.com/pypa/virtualenv/commit/35f0b33562790176dd186d0a3c517af3acc7a0e1 [2] https://github.com/pypa/virtualenv/security/advisories/GHSA-fc2g-4w5p-6whm 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
2d3e0c1b5d
commit
f11354004d
2 files changed
+157
No files matched your search
@@ -0,0 +1,156 @@
|
||||
From 35f0b33562790176dd186d0a3c517af3acc7a0e1 Mon Sep 17 00:00:00 2001
|
||||
From: Bernát Gábor <gaborjbernat@gmail.com>
|
||||
Date: Sat, 17 Oct 2026 02:57:36 -0700
|
||||
Subject: [PATCH] fix(path): apply current permissions to copied files
|
||||
(#3425)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
CVE: CVE-2026-113720
|
||||
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/35f0b33562790176dd186d0a3c517af3acc7a0e1]
|
||||
|
||||
Backport Changes:
|
||||
- Import Final without TYPE_CHECKING and remove the now-empty type-only
|
||||
import block: moving Path to runtime leaves no type-only imports in
|
||||
21.2.1's copy helper.
|
||||
- Add the copy and Final imports needed by the unchanged upstream
|
||||
regression tests; these imports are absent from 21.2.1's test file.
|
||||
- Omit the upstream release changelog fragment.
|
||||
|
||||
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||
---
|
||||
src/virtualenv/util/path/_sync.py | 35 +++++++++++++----------
|
||||
tests/unit/test_util.py | 47 ++++++++++++++++++++++++++++++-
|
||||
2 files changed, 66 insertions(+), 16 deletions(-)
|
||||
|
||||
diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py
|
||||
index 6d701b9..a94466f 100644
|
||||
--- a/src/virtualenv/util/path/_sync.py
|
||||
+++ b/src/virtualenv/util/path/_sync.py
|
||||
@@ -4,11 +4,9 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
-from stat import S_IWUSR
|
||||
-from typing import TYPE_CHECKING
|
||||
-
|
||||
-if TYPE_CHECKING:
|
||||
- from pathlib import Path
|
||||
+from pathlib import Path
|
||||
+from stat import S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR
|
||||
+from typing import Final
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,22 +38,29 @@ def symlink(src: Path, dest: Path) -> None:
|
||||
|
||||
|
||||
def copy(src: Path, dest: Path) -> None:
|
||||
- ensure_safe_to_do(src, dest)
|
||||
- is_dir = src.is_dir()
|
||||
- method = copytree if is_dir else shutil.copy
|
||||
LOGGER.debug("copy %s", _Debug(src, dest))
|
||||
- method(str(src), str(dest))
|
||||
+ if src.is_dir():
|
||||
+ ensure_safe_to_do(src, dest)
|
||||
+ copytree(str(src), str(dest))
|
||||
+ else:
|
||||
+ _copy_file(src, dest)
|
||||
|
||||
|
||||
def copytree(src: str, dest: str) -> None:
|
||||
for root, _, files in os.walk(src):
|
||||
- dest_dir = os.path.join(dest, os.path.relpath(root, src))
|
||||
- if not os.path.isdir(dest_dir):
|
||||
- os.makedirs(dest_dir)
|
||||
+ dest_dir: Final = Path(dest) / os.path.relpath(root, src)
|
||||
+ dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
for name in files:
|
||||
- src_f = os.path.join(root, name)
|
||||
- dest_f = os.path.join(dest_dir, name)
|
||||
- shutil.copy(src_f, dest_f)
|
||||
+ _copy_file(Path(root) / name, dest_dir / name)
|
||||
+
|
||||
+
|
||||
+def _copy_file(src: Path, dest: Path) -> None:
|
||||
+ ensure_safe_to_do(src, dest)
|
||||
+ mode: Final = 0o755 if src.stat().st_mode & (S_IXUSR | S_IXGRP | S_IXOTH) else 0o644
|
||||
+ flags: Final = os.O_WRONLY | os.O_CREAT | os.O_EXCL | (os.O_BINARY if os.name == "nt" else 0)
|
||||
+ # Creation applies the current umask without changing the process-wide setting.
|
||||
+ with src.open("rb") as source, os.fdopen(os.open(str(dest), flags, mode), "wb") as target:
|
||||
+ shutil.copyfileobj(source, target, length=1024 * 1024)
|
||||
|
||||
|
||||
def safe_delete(dest: Path) -> None:
|
||||
diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py
|
||||
index 2cdc038..ea7030b 100644
|
||||
--- a/tests/unit/test_util.py
|
||||
+++ b/tests/unit/test_util.py
|
||||
@@ -3,18 +3,63 @@ from __future__ import annotations
|
||||
import concurrent.futures
|
||||
import os
|
||||
import traceback
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import TYPE_CHECKING, Final
|
||||
|
||||
import pytest
|
||||
|
||||
from virtualenv.app_data import _cache_dir_with_migration, _default_app_data_dir
|
||||
from virtualenv.util.lock import ReentrantFileLock
|
||||
+from virtualenv.util.path import copy
|
||||
from virtualenv.util.subprocess import run_cmd
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
+@pytest.mark.skipif(os.name != "posix", reason="POSIX permission bits")
|
||||
+@pytest.mark.parametrize("directory", [pytest.param(False, id="file"), pytest.param(True, id="tree")])
|
||||
+@pytest.mark.parametrize(
|
||||
+ ("source_mode", "mask", "expected"),
|
||||
+ [
|
||||
+ pytest.param(0o666, 0o022, 0o644, id="normal"),
|
||||
+ pytest.param(0o666, 0o077, 0o600, id="private"),
|
||||
+ pytest.param(0o444, 0o022, 0o644, id="read-only-source"),
|
||||
+ pytest.param(0o600, 0o022, 0o644, id="private-source"),
|
||||
+ pytest.param(0o777, 0o022, 0o755, id="executable"),
|
||||
+ pytest.param(0o777, 0o077, 0o700, id="private-executable"),
|
||||
+ pytest.param(0o666, 0o000, 0o644, id="permissive-umask"),
|
||||
+ pytest.param(0o777, 0o000, 0o755, id="permissive-executable"),
|
||||
+ pytest.param(0o666, 0o002, 0o644, id="group-umask"),
|
||||
+ pytest.param(0o777, 0o002, 0o755, id="group-executable"),
|
||||
+ ],
|
||||
+)
|
||||
+def test_copy_uses_current_umask(tmp_path: Path, directory: bool, source_mode: int, mask: int, expected: int) -> None:
|
||||
+ source: Final = tmp_path / "source"
|
||||
+ source.mkdir()
|
||||
+ payload: Final = source / "entry"
|
||||
+ payload.write_bytes(b"copied bytes")
|
||||
+ payload.chmod(source_mode)
|
||||
+ destination: Final = tmp_path / "destination"
|
||||
+ previous: Final = os.umask(mask)
|
||||
+ try:
|
||||
+ copy(source if directory else payload, destination)
|
||||
+ finally:
|
||||
+ os.umask(previous)
|
||||
+ result: Final = destination / "entry" if directory else destination
|
||||
+ assert (result.read_bytes(), result.stat().st_mode & 0o777) == (b"copied bytes", expected)
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize("directory", [pytest.param(False, id="file"), pytest.param(True, id="tree")])
|
||||
+def test_copy_replaces_contents(tmp_path: Path, directory: bool) -> None:
|
||||
+ source: Final = tmp_path / "source"
|
||||
+ source.mkdir()
|
||||
+ (source / "entry").write_bytes(b"new")
|
||||
+ destination: Final = tmp_path / "destination"
|
||||
+ destination.write_bytes(b"old")
|
||||
+ copy(source if directory else source / "entry", destination)
|
||||
+ assert (destination / "entry" if directory else destination).read_bytes() == b"new"
|
||||
+
|
||||
+
|
||||
def test_run_fail(tmp_path) -> None:
|
||||
code, out, err = run_cmd([str(tmp_path)])
|
||||
assert err
|
||||
--
|
||||
2.35.6
|
||||
@@ -24,6 +24,7 @@ SRC_URI += " \
|
||||
file://CVE-2026-113723.patch \
|
||||
file://CVE-2026-115586.patch \
|
||||
file://CVE-2026-113715.patch \
|
||||
file://CVE-2026-113720.patch \
|
||||
"
|
||||
|
||||
BBCLASSEXTEND = "native nativesdk"
|
||||
|
||||
Reference in new issue
Block a user