Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
365 changes: 365 additions & 0 deletions SPECS/python-virtualenv/CVE-2026-6357v0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
From 8786d084768e0b42afa093083c8c05ee8e525574 Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com>
Date: Fri, 17 Apr 2026 09:52:38 -0400
Subject: [PATCH 1/3] Split the pip self-version check to get info before run

Upstream Patch Reference: https://patch-diff.githubusercontent.com/raw/pypa/pip/pull/13923.patch
---
news/13923.trivial.rst | 2 +
pip/_internal/cli/base_command.py | 10 ++--
pip/_internal/cli/index_command.py | 34 +++++++++++---
pip/_internal/commands/install.py | 22 +++++++++
pip/_internal/commands/list.py | 12 +++--
pip/_internal/self_outdated_check.py | 69 +++++++++++++---------------
6 files changed, 98 insertions(+), 51 deletions(-)
create mode 100644 news/13923.trivial.rst

diff --git a/news/13923.trivial.rst b/news/13923.trivial.rst
new file mode 100644
index 0000000..cb3b0ff
--- /dev/null
+++ b/news/13923.trivial.rst
@@ -0,0 +1,2 @@
+Split the pip self-version check into a fetch phase before the command
+body and an emit phase afterwards.
diff --git a/pip/_internal/cli/base_command.py b/pip/_internal/cli/base_command.py
index 362f84b..6c2594e 100644
--- a/pip/_internal/cli/base_command.py
+++ b/pip/_internal/cli/base_command.py
@@ -1,11 +1,13 @@
"""Base Command class, and related routines"""

+import contextlib
import logging
import logging.config
import optparse
import os
import sys
import traceback
+from collections.abc import Iterator
from optparse import Values
from typing import List, Optional, Tuple

@@ -79,7 +81,8 @@ class Command(CommandContextMixIn):
def add_options(self) -> None:
pass

- def handle_pip_version_check(self, options: Values) -> None:
+ @contextlib.contextmanager
+ def pip_version_check(self, options: Values, args: List[str]) -> Iterator[None]:
"""
This is a no-op so that commands by default do not do the pip version
check.
@@ -87,16 +90,15 @@ class Command(CommandContextMixIn):
# Make sure we do the pip version check if the index_group options
# are present.
assert not hasattr(options, "no_index")
+ yield

def run(self, options: Values, args: List[str]) -> int:
raise NotImplementedError

def _run_wrapper(self, level_number: int, options: Values, args: List[str]) -> int:
def _inner_run() -> int:
- try:
+ with self.pip_version_check(options, args):
return self.run(options, args)
- finally:
- self.handle_pip_version_check(options)

if options.debug_mode:
rich_traceback.install(show_locals=True)
diff --git a/pip/_internal/cli/index_command.py b/pip/_internal/cli/index_command.py
index 295108e..e559316 100644
--- a/pip/_internal/cli/index_command.py
+++ b/pip/_internal/cli/index_command.py
@@ -6,8 +6,10 @@ so commands which don't always hit the network (e.g. list w/o --outdated or
--uptodate) don't need waste time importing PipSession and friends.
"""

+import contextlib
import logging
import os
+from collections.abc import Iterator
import sys
from optparse import Values
from typing import TYPE_CHECKING, List, Optional
@@ -21,6 +23,7 @@ if TYPE_CHECKING:
from ssl import SSLContext

from pip._internal.network.session import PipSession
+ from pip._internal.self_outdated_check import UpgradePrompt

logger = logging.getLogger(__name__)

@@ -132,10 +135,18 @@ class SessionCommandMixin(CommandContextMixIn):
return session


-def _pip_self_version_check(session: "PipSession", options: Values) -> None:
- from pip._internal.self_outdated_check import pip_self_version_check as check
+def _pip_self_version_check_fetch(
+ session: PipSession, options: Values
+-> Optional["UpgradePrompt"]:
+ from pip._internal.self_outdated_check import pip_self_version_check_fetch

- check(session, options)
+ return pip_self_version_check_fetch(session, options)
+
+
+def _pip_self_version_check_emit(upgrade_prompt: Optional["UpgradePrompt"]) -> None:
+ from pip._internal.self_outdated_check import pip_self_version_check_emit
+
+ pip_self_version_check_emit(upgrade_prompt)


class IndexGroupCommand(Command, SessionCommandMixin):
@@ -145,7 +156,8 @@ class IndexGroupCommand(Command, SessionCommandMixin):
This also corresponds to the commands that permit the pip version check.
"""

- def handle_pip_version_check(self, options: Values) -> None:
+ @contextlib.contextmanager
+ def pip_version_check(self, options: Values, args: List[str]) -> Iterator[None]:
"""
Do the pip version check if not disabled.

@@ -155,17 +167,27 @@ class IndexGroupCommand(Command, SessionCommandMixin):
assert hasattr(options, "no_index")

if options.disable_pip_version_check or options.no_index:
+ yield
return

+ upgrade_prompt: Optional["UpgradePrompt"] = None
try:
- # Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
options,
retries=0,
timeout=min(5, options.timeout),
)
with session:
- _pip_self_version_check(session, options)
+ upgrade_prompt = _pip_self_version_check_fetch(session, options)
except Exception:
logger.warning("There was an error checking the latest version of pip.")
logger.debug("See below for error", exc_info=True)
+
+ try:
+ yield
+ finally:
+ try:
+ _pip_self_version_check_emit(upgrade_prompt)
+ except Exception:
+ logger.warning("There was an error checking the latest version of pip.")
+ logger.debug("See below for error", exc_info=True)
diff --git a/pip/_internal/commands/install.py b/pip/_internal/commands/install.py
index 232a34a..7341044 100644
--- a/pip/_internal/commands/install.py
+++ b/pip/_internal/commands/install.py
@@ -1,12 +1,15 @@
+import contextlib
import errno
import json
import operator
import os
import shutil
import site
+from collections.abc import Iterator
from optparse import SUPPRESS_HELP, Values
from typing import List, Optional

+from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.rich import print_json

@@ -57,6 +60,14 @@ from pip._internal.wheel_builder import build, should_build_for_install_command
logger = getLogger(__name__)


+def _arg_refers_to_pip(arg: str) -> bool:
+ try:
+ req = Requirement(arg)
+ except InvalidRequirement:
+ return False
+ return canonicalize_name(req.name) == "pip"
+
+
class InstallCommand(RequirementCommand):
"""
Install packages from:
@@ -270,6 +281,17 @@ class InstallCommand(RequirementCommand):
),
)

+ @contextlib.contextmanager
+ def pip_version_check(self, options: Values, args: List[str]) -> Iterator[None]:
+ # Skip the self-version check when pip itself is a requirement. The
+ # running pip may be replaced mid-command, and the upgrade prompt
+ # is redundant.
+ if any(_arg_refers_to_pip(arg) for arg in args):
+ yield
+ return
+ with super().pip_version_check(options, args):
+ yield
+
@with_cleanup
def run(self, options: Values, args: List[str]) -> int:
if options.use_user_site and options.target_dir is not None:
diff --git a/pip/_internal/commands/list.py b/pip/_internal/commands/list.py
index 8494370..102bfc1 100644
--- a/pip/_internal/commands/list.py
+++ b/pip/_internal/commands/list.py
@@ -1,5 +1,7 @@
+import contextlib
import json
import logging
+from collections.abc import Iterator
from optparse import Values
from typing import TYPE_CHECKING, Generator, List, Optional, Sequence, Tuple, cast

@@ -134,9 +136,13 @@ class ListCommand(IndexGroupCommand):
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)

- def handle_pip_version_check(self, options: Values) -> None:
- if options.outdated or options.uptodate:
- super().handle_pip_version_check(options)
+ @contextlib.contextmanager
+ def pip_version_check(self, options: Values, args: List[str]) -> Iterator[None]:
+ if not (options.outdated or options.uptodate):
+ yield
+ return
+ with super().pip_version_check(options, args):
+ yield

def _build_package_finder(
self, options: Values, session: "PipSession"
diff --git a/pip/_internal/self_outdated_check.py b/pip/_internal/self_outdated_check.py
index 2e0e3df..5c3f8ee 100644
--- a/pip/_internal/self_outdated_check.py
+++ b/pip/_internal/self_outdated_check.py
@@ -1,5 +1,4 @@
import datetime
-import functools
import hashlib
import json
import logging
@@ -7,7 +6,7 @@ import optparse
import os.path
import sys
from dataclasses import dataclass
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Dict, Optional

from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import parse as parse_version
@@ -153,16 +152,6 @@ class UpgradePrompt:
)


-def was_installed_by_pip(pkg: str) -> bool:
- """Checks whether pkg was installed by pip
-
- This is used not to display the upgrade message when pip is in fact
- installed by system package manager, such as dnf on Fedora.
- """
- dist = get_default_environment().get_distribution(pkg)
- return dist is not None and "pip" == dist.installer
-
-
def _get_current_remote_pip_version(
session: PipSession, options: optparse.Values
) -> Optional[str]:
@@ -191,28 +180,16 @@ def _get_current_remote_pip_version(
return str(best_candidate.version)


-def _self_version_check_logic(
- *,
- state: SelfCheckState,
- current_time: datetime.datetime,
- local_version: Version,
- get_remote_version: Callable[[], Optional[str]],
+def _compute_upgrade_prompt(
+ local_version: Version, remote_version_str: str, installed_by_pip: bool
) -> Optional[UpgradePrompt]:
- remote_version_str = state.get(current_time)
- if remote_version_str is None:
- remote_version_str = get_remote_version()
- if remote_version_str is None:
- logger.debug("No remote pip version found")
- return None
- state.set(remote_version_str, current_time)

remote_version = parse_version(remote_version_str)
logger.debug("Remote version of pip: %s", remote_version)
logger.debug("Local version of pip: %s", local_version)
+ logger.debug("Was pip installed by pip? %s", installed_by_pip)

- pip_installed_by_pip = was_installed_by_pip("pip")
- logger.debug("Was pip installed by pip? %s", pip_installed_by_pip)
- if not pip_installed_by_pip:
+ if not installed_by_pip:
return None # Only suggest upgrade if pip is installed by pip.

local_version_is_older = (
@@ -225,28 +202,44 @@ def _self_version_check_logic(
return None


-def pip_self_version_check(session: PipSession, options: optparse.Values) -> None:
- """Check for an update for pip.
+def pip_self_version_check_fetch(
+ session: PipSession, options: optparse.Values
+) -> Optional["UpgradePrompt"]:
+ """Compute the pip upgrade prompt, if any, before the command runs.

Limit the frequency of checks to once per week. State is stored either in
the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
of the pip script path.
+
+ Pair with :func:`pip_self_version_check_emit`, which displays the prompt
+ after the command body runs.
"""
installed_dist = get_default_environment().get_distribution("pip")
if not installed_dist:
- return
+ return None
try:
check_externally_managed()
except ExternallyManagedEnvironment:
- return
+ return None

- upgrade_prompt = _self_version_check_logic(
- state=SelfCheckState(cache_dir=options.cache_dir),
- current_time=datetime.datetime.now(datetime.timezone.utc),
+ state = SelfCheckState(cache_dir=options.cache_dir)
+ current_time = datetime.datetime.now(datetime.timezone.utc)
+ remote_version_str = state.get(current_time)
+ if remote_version_str is None:
+ remote_version_str = _get_current_remote_pip_version(session, options)
+ if remote_version_str is None:
+ logger.debug("No remote pip version found")
+ return None
+ state.set(remote_version_str, current_time)
+
+ return _compute_upgrade_prompt(
local_version=installed_dist.version,
- get_remote_version=functools.partial(
- _get_current_remote_pip_version, session, options
- ),
+ remote_version_str=remote_version_str,
+ installed_by_pip=installed_dist.installer == "pip",
)
+
+
+def pip_self_version_check_emit(upgrade_prompt: Optional["UpgradePrompt"]) -> None:
+ """Emit the upgrade prompt captured by :func:`pip_self_version_check_fetch`."""
if upgrade_prompt is not None:
logger.warning("%s", upgrade_prompt, extra={"rich": True})
--
2.43.0

Loading
Loading