diff --git a/backport-CVE-2023-45803-Made-body-stripped-from-HTTP-requests.patch b/backport-CVE-2023-45803-Made-body-stripped-from-HTTP-requests.patch new file mode 100644 index 0000000000000000000000000000000000000000..e7a6cae784cc3e592012189b4697ff0974837bb1 --- /dev/null +++ b/backport-CVE-2023-45803-Made-body-stripped-from-HTTP-requests.patch @@ -0,0 +1,99 @@ +From b594c5ceaca38e1ac215f916538fb128e3526a36 Mon Sep 17 00:00:00 2001 +From: Illia Volochii +Date: Tue, 17 Oct 2023 19:35:39 +0300 +Subject: [PATCH] Merge pull request from GHSA-g4mx-q9vg-27p4 + +Conflict:Files dummyserver/handlers.py, test/with_dummyserver/test_connectionpool.py +and test/with_dummyserver/test_poolmanager.py do not exist. Therefore, no dummy server +and test case is involved. +Reference:https://github.com/urllib3/urllib3/commit/b594c5ceaca38e1ac215f916538fb128e3526a36 + +--- + src/pip/_vendor/urllib3/_collections.py | 18 ++++++++++++++++++ + src/pip/_vendor/urllib3/connectionpool.py | 5 +++++ + src/pip/_vendor/urllib3/poolmanager.py | 7 +++++-- + 3 files changed, 28 insertions(+), 2 deletions(-) + +diff --git a/src/pip/_vendor/urllib3/_collections.py b/src/pip/_vendor/urllib3/_collections.py +index da9857e..bceb845 100644 +--- a/src/pip/_vendor/urllib3/_collections.py ++++ b/src/pip/_vendor/urllib3/_collections.py +@@ -268,6 +268,24 @@ class HTTPHeaderDict(MutableMapping): + else: + return vals[1:] + ++ def _prepare_for_method_change(self): ++ """ ++ Remove content-specific header fields before changing the request ++ method to GET or HEAD according to RFC 9110, Section 15.4. ++ """ ++ content_specific_headers = [ ++ "Content-Encoding", ++ "Content-Language", ++ "Content-Location", ++ "Content-Type", ++ "Content-Length", ++ "Digest", ++ "Last-Modified", ++ ] ++ for header in content_specific_headers: ++ self.discard(header) ++ return self ++ + # Backwards compatibility for httplib + getheaders = getlist + getallmatchingheaders = getlist +diff --git a/src/pip/_vendor/urllib3/connectionpool.py b/src/pip/_vendor/urllib3/connectionpool.py +index 96844d9..5a6adcb 100644 +--- a/src/pip/_vendor/urllib3/connectionpool.py ++++ b/src/pip/_vendor/urllib3/connectionpool.py +@@ -9,6 +9,7 @@ import warnings + from socket import error as SocketError + from socket import timeout as SocketTimeout + ++from ._collections import HTTPHeaderDict + from .connection import ( + BaseSSLError, + BrokenPipeError, +@@ -843,7 +844,11 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): + redirect_location = redirect and response.get_redirect_location() + if redirect_location: + if response.status == 303: ++ # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" ++ # And lose the body not to transfer anything sensitive. ++ body = None ++ headers = HTTPHeaderDict(headers)._prepare_for_method_change() + + try: + retries = retries.increment(method, url, response=response, _pool=self) +diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py +index 14b10da..fb51bf7 100644 +--- a/src/pip/_vendor/urllib3/poolmanager.py ++++ b/src/pip/_vendor/urllib3/poolmanager.py +@@ -4,7 +4,7 @@ import collections + import functools + import logging + +-from ._collections import RecentlyUsedContainer ++from ._collections import HTTPHeaderDict, RecentlyUsedContainer + from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme + from .exceptions import ( + LocationValueError, +@@ -382,9 +382,12 @@ class PoolManager(RequestMethods): + # Support relative URLs for redirecting. + redirect_location = urljoin(url, redirect_location) + +- # RFC 7231, Section 6.4.4 + if response.status == 303: ++ # Change the method according to RFC 9110, Section 15.4.4. + method = "GET" ++ # And lose the body not to transfer anything sensitive. ++ kw["body"] = None ++ kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() + + retries = kw.get("retries") + if not isinstance(retries, Retry): +-- +2.26.2.windows.1 + diff --git a/backport-CVE-2024-37891-Strip-Proxy-Authorization-header-on-redirects.patch b/backport-CVE-2024-37891-Strip-Proxy-Authorization-header-on-redirects.patch new file mode 100644 index 0000000000000000000000000000000000000000..4f8a34f2a524ca5993bc21c2f5df43e4f20b550e --- /dev/null +++ b/backport-CVE-2024-37891-Strip-Proxy-Authorization-header-on-redirects.patch @@ -0,0 +1,33 @@ +From accff72ecc2f6cf5a76d9570198a93ac7c90270e Mon Sep 17 00:00:00 2001 +From: Quentin Pradet +Date: Mon, 17 Jun 2024 11:09:06 +0400 +Subject: [PATCH] Merge pull request from GHSA-34jh-p97f-mpxf + +* Strip Proxy-Authorization header on redirects + +Conflict:Files test/test_retry.py and test/with_dummyserver/test_poolmanager.py do not +exist. Therefore, no test case is involved. +Reference:https://github.com/urllib3/urllib3/commit/accff72ecc2f6cf5a76d9570198a93ac7c90270e + +--- + src/pip/_vendor/urllib3/util/retry.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/src/pip/_vendor/urllib3/util/retry.py b/src/pip/_vendor/urllib3/util/retry.py +index 60ef6c4..9a1e90d 100644 +--- a/src/pip/_vendor/urllib3/util/retry.py ++++ b/src/pip/_vendor/urllib3/util/retry.py +@@ -235,7 +235,9 @@ class Retry(object): + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Default headers to be used for ``remove_headers_on_redirect`` +- DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Cookie", "Authorization"]) ++ DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset( ++ ["Cookie", "Authorization", "Proxy-Authorization"] ++ ) + + #: Maximum backoff time. + DEFAULT_BACKOFF_MAX = 120 +-- +2.26.2.windows.1 + diff --git a/backport-CVE-2024-47081.patch b/backport-CVE-2024-47081.patch new file mode 100644 index 0000000000000000000000000000000000000000..986ac410e5b615bfdf2ba07f7dfaffe164b1dbd1 --- /dev/null +++ b/backport-CVE-2024-47081.patch @@ -0,0 +1,34 @@ +From 96ba401c1296ab1dda74a2365ef36d88f7d144ef Mon Sep 17 00:00:00 2001 +From: Nate Prewitt +Date: Wed, 25 Sep 2024 08:03:20 -0700 +Subject: [PATCH] Only use hostname to do netrc lookup instead of netloc + +Conflict:NA +Reference:https://github.com/psf/requests/commit/96ba401c1296ab1dda74a2365ef36d88f7d144ef + +--- + src/pip/_vendor/requests/utils.py | 8 +------- + 1 file changed, 1 insertion(+), 7 deletions(-) + +diff --git a/src/pip/_vendor/requests/utils.py b/src/pip/_vendor/requests/utils.py +index fcb9966..abffd5b 100644 +--- a/src/pip/_vendor/requests/utils.py ++++ b/src/pip/_vendor/requests/utils.py +@@ -204,13 +204,7 @@ def get_netrc_auth(url, raise_errors=False): + return + + ri = urlparse(url) +- +- # Strip port numbers from netloc. This weird `if...encode`` dance is +- # used for Python 3.2, which doesn't support unicode literals. +- splitstr = b":" +- if isinstance(url, str): +- splitstr = splitstr.decode("ascii") +- host = ri.netloc.split(splitstr)[0] ++ host = ri.hostname + + try: + _netrc = netrc(netrc_path).authenticators(host) +-- +2.33.0 + diff --git a/backport-CVE-2025-50181.patch b/backport-CVE-2025-50181.patch new file mode 100644 index 0000000000000000000000000000000000000000..1235da75d55cc2f2088779b18a036e05703f04e0 --- /dev/null +++ b/backport-CVE-2025-50181.patch @@ -0,0 +1,90 @@ +From f05b1329126d5be6de501f9d1e3e36738bc08857 Mon Sep 17 00:00:00 2001 +From: Illia Volochii +Date: Wed, 18 Jun 2025 16:25:01 +0300 +Subject: [PATCH] Merge commit from fork + +* Apply Quentin's suggestion + +Co-authored-by: Quentin Pradet + +* Add tests for disabled redirects in the pool manager + +* Add a possible fix for the issue with not raised `MaxRetryError` + +* Make urllib3 handle redirects instead of JS when JSPI is used + +* Fix info in the new comment + +* State that redirects with XHR are not controlled by urllib3 + +* Remove excessive params from new test requests + +* Add tests reaching max non-0 redirects + +* Test redirects with Emscripten + +* Fix `test_merge_pool_kwargs` + +* Add a changelog entry + +* Parametrize tests + +* Drop a fix for Emscripten + +* Apply Seth's suggestion to docs + +Co-authored-by: Seth Michael Larson + +* Use a minor release instead of the patch one + +Reference:https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857 +Conflict:test/with_dummyserver/test_poolmanger, +test/contrib/emscripten/test_emscrepten.py has not been modified because +is has been deleted in the pre-phase of the spec file;CHANGES.rst, +docs/reference/contrib/emscripten.rst,dummyserver/app.py has not been +modified because these are the latest features and informations, does +not involve related modifications + +--- + src/pip/_vendor/urllib3/poolmanager.py | 18 +++++++++++++++++- + 1 file changed, 17 insertions(+), 1 deletion(-) + +diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py +index fb51bf7..a8de7c6 100644 +--- a/src/pip/_vendor/urllib3/poolmanager.py ++++ b/src/pip/_vendor/urllib3/poolmanager.py +@@ -170,6 +170,22 @@ class PoolManager(RequestMethods): + + def __init__(self, num_pools=10, headers=None, **connection_pool_kw): + RequestMethods.__init__(self, headers) ++ if "retries" in connection_pool_kw: ++ retries = connection_pool_kw["retries"] ++ if not isinstance(retries, Retry): ++ # When Retry is initialized, raise_on_redirect is based ++ # on a redirect boolean value. ++ # But requests made via a pool manager always set ++ # redirect to False, and raise_on_redirect always ends ++ # up being False consequently. ++ # Here we fix the issue by setting raise_on_redirect to ++ # a value needed by the pool manager without considering ++ # the redirect boolean. ++ raise_on_redirect = retries is not False ++ retries = Retry.from_int(retries, redirect=False) ++ retries.raise_on_redirect = raise_on_redirect ++ connection_pool_kw = connection_pool_kw.copy() ++ connection_pool_kw["retries"] = retries + self.connection_pool_kw = connection_pool_kw + self.pools = RecentlyUsedContainer(num_pools) + +@@ -389,7 +405,7 @@ class PoolManager(RequestMethods): + kw["body"] = None + kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() + +- retries = kw.get("retries") ++ retries = kw.get("retries", response.retries) + if not isinstance(retries, Retry): + retries = Retry.from_int(retries, redirect=redirect) + +-- +2.33.0 + diff --git a/dummy-certifi.patch b/dummy-certifi.patch new file mode 100644 index 0000000000000000000000000000000000000000..8896ce80f2f64e8941e99d875eab4f1208f327ff --- /dev/null +++ b/dummy-certifi.patch @@ -0,0 +1,128 @@ +From 09c983fdeabe3fa0b90b73f32ddf84a61e498e09 Mon Sep 17 00:00:00 2001 +From: Karolina Surma +Date: Tue, 15 Nov 2022 09:22:46 +0100 +Subject: [PATCH] Dummy certifi patch + +--- + src/pip/_vendor/certifi/core.py | 105 ++------------------------------ + 1 file changed, 6 insertions(+), 99 deletions(-) + +diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py +index c3e5466..eb297f7 100644 +--- a/src/pip/_vendor/certifi/core.py ++++ b/src/pip/_vendor/certifi/core.py +@@ -4,105 +4,12 @@ certifi.py + + This module returns the installation location of cacert.pem or its contents. + """ +-import sys + ++# The RPM-packaged certifi always uses the system certificates ++def where() -> str: ++ return '/etc/pki/tls/certs/ca-bundle.crt' + +-if sys.version_info >= (3, 11): ++def contents() -> str: ++ with open(where(), encoding='utf=8') as data: ++ return data.read() + +- from importlib.resources import as_file, files +- +- _CACERT_CTX = None +- _CACERT_PATH = None +- +- def where() -> str: +- # This is slightly terrible, but we want to delay extracting the file +- # in cases where we're inside of a zipimport situation until someone +- # actually calls where(), but we don't want to re-extract the file +- # on every call of where(), so we'll do it once then store it in a +- # global variable. +- global _CACERT_CTX +- global _CACERT_PATH +- if _CACERT_PATH is None: +- # This is slightly janky, the importlib.resources API wants you to +- # manage the cleanup of this file, so it doesn't actually return a +- # path, it returns a context manager that will give you the path +- # when you enter it and will do any cleanup when you leave it. In +- # the common case of not needing a temporary file, it will just +- # return the file system location and the __exit__() is a no-op. +- # +- # We also have to hold onto the actual context manager, because +- # it will do the cleanup whenever it gets garbage collected, so +- # we will also store that at the global level as well. +- _CACERT_CTX = as_file(files("pip._vendor.certifi").joinpath("cacert.pem")) +- _CACERT_PATH = str(_CACERT_CTX.__enter__()) +- +- return _CACERT_PATH +- +- def contents() -> str: +- return files("pip._vendor.certifi").joinpath("cacert.pem").read_text(encoding="ascii") +- +-elif sys.version_info >= (3, 7): +- +- from importlib.resources import path as get_path, read_text +- +- _CACERT_CTX = None +- _CACERT_PATH = None +- +- def where() -> str: +- # This is slightly terrible, but we want to delay extracting the +- # file in cases where we're inside of a zipimport situation until +- # someone actually calls where(), but we don't want to re-extract +- # the file on every call of where(), so we'll do it once then store +- # it in a global variable. +- global _CACERT_CTX +- global _CACERT_PATH +- if _CACERT_PATH is None: +- # This is slightly janky, the importlib.resources API wants you +- # to manage the cleanup of this file, so it doesn't actually +- # return a path, it returns a context manager that will give +- # you the path when you enter it and will do any cleanup when +- # you leave it. In the common case of not needing a temporary +- # file, it will just return the file system location and the +- # __exit__() is a no-op. +- # +- # We also have to hold onto the actual context manager, because +- # it will do the cleanup whenever it gets garbage collected, so +- # we will also store that at the global level as well. +- _CACERT_CTX = get_path("pip._vendor.certifi", "cacert.pem") +- _CACERT_PATH = str(_CACERT_CTX.__enter__()) +- +- return _CACERT_PATH +- +- def contents() -> str: +- return read_text("pip._vendor.certifi", "cacert.pem", encoding="ascii") +- +-else: +- import os +- import types +- from typing import Union +- +- Package = Union[types.ModuleType, str] +- Resource = Union[str, "os.PathLike"] +- +- # This fallback will work for Python versions prior to 3.7 that lack the +- # importlib.resources module but relies on the existing `where` function +- # so won't address issues with environments like PyOxidizer that don't set +- # __file__ on modules. +- def read_text( +- package: Package, +- resource: Resource, +- encoding: str = 'utf-8', +- errors: str = 'strict' +- ) -> str: +- with open(where(), encoding=encoding) as data: +- return data.read() +- +- # If we don't have importlib.resources, then we will just do the old logic +- # of assuming we're on the filesystem and munge the path directly. +- def where() -> str: +- f = os.path.dirname(__file__) +- +- return os.path.join(f, "cacert.pem") +- +- def contents() -> str: +- return read_text("pip._vendor.certifi", "cacert.pem", encoding="ascii") +-- +2.37.3 + diff --git a/pip-23.3.1.tar.gz b/pip-23.3.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6eb1137a5b69926108d80f741a0c2186a2d514b7 Binary files /dev/null and b/pip-23.3.1.tar.gz differ diff --git a/pip-25.3.tar.gz b/pip-25.3.tar.gz deleted file mode 100644 index dd946656907df6a71b6b23982f0d772716912102..0000000000000000000000000000000000000000 Binary files a/pip-25.3.tar.gz and /dev/null differ diff --git a/python-pip.spec b/python-pip.spec index fd5e28efaa01e4b06fbf4dfaa092e98372e23d29..6549dd46e5eabee19803c2ca7c44e1937c82649e 100644 --- a/python-pip.spec +++ b/python-pip.spec @@ -5,25 +5,30 @@ pip is the package installer for Python. You can use pip to install packages from the Python Package Index and other indexes. %global bashcompdir %(b=$(pkg-config --variable=completionsdir bash-completion 2>/dev/null); echo ${b:-%{_sysconfdir}/bash_completion.d}) Name: python-%{srcname} -Version: 25.3 -Release: 1 +Version: 23.3.1 +Release: 5 Summary: A tool for installing and managing Python packages License: MIT and Python and ASL 2.0 and BSD and ISC and LGPLv2 and MPLv2.0 and (ASL 2.0 or BSD) URL: http://www.pip-installer.org Source0: %{pypi_source} Source1: pip.loongarch.conf -Source10: pip-allow-older-versions.patch BuildArch: noarch +Patch1: remove-existing-dist-only-if-path-conflicts.patch +Patch6000: dummy-certifi.patch +Patch6001: backport-CVE-2023-45803-Made-body-stripped-from-HTTP-requests.patch +Patch6002: backport-CVE-2024-37891-Strip-Proxy-Authorization-header-on-redirects.patch +Patch6003: backport-CVE-2024-47081.patch +Patch6004: backport-CVE-2025-50181.patch + +Source10: pip-allow-older-versions.patch %description %{_description} %package -n python%{python3_pkgversion}-%{srcname} Summary: %{summary} -BuildRequires: python%{python3_pkgversion}-devel python%{python3_pkgversion}-setuptools bash-completion ca-certificates -BuildRequires: python3-pip +BuildRequires: python%{python3_pkgversion}-devel python%{python3_pkgversion}-setuptools bash-completion ca-certificates Requires: python%{python3_pkgversion}-setuptools ca-certificates BuildRequires: python%{python3_pkgversion}-wheel -BuildRequires: python3-flit-core %{?python_provide:%python_provide python%{python3_pkgversion}-%{srcname}} Provides: python%{python3_pkgversion}dist(pip) = %{version} Provides: python%{python3_version}dist(pip) = %{version} @@ -48,13 +53,21 @@ sed -i '/html_theme = "furo"/d' docs/html/conf.py # Remove windows executable binaries rm -v src/pip/_vendor/distlib/*.exe +sed -i '/\.exe/d' setup.py %build -%pyproject_build +%py3_build_wheel %install -%pyproject_install +%{__python3} dist/%{python_wheelname}/pip install \ + --root %{buildroot} \ + --no-deps \ + --no-cache-dir \ + --no-index \ + --ignore-installed \ + --find-links dist \ + 'pip==%{version}' %if %{with doc} pushd docs/build/man @@ -93,6 +106,7 @@ echo rpm > %{buildroot}%{python3_sitelib}/pip-%{version}.dist-info/INSTALLER rm %{buildroot}%{python3_sitelib}/pip-%{version}.dist-info/RECORD mkdir -p %{buildroot}%{python_wheeldir} +install -p dist/%{python_wheelname} -t %{buildroot}%{python_wheeldir} # Set default pip mirror via pip.conf %ifarch loongarch64 @@ -119,16 +133,9 @@ install -D -m0644 %{SOURCE1} %{buildroot}%{_sysconfdir}/pip.conf %files wheel %license LICENSE.txt %dir %{python_wheeldir}/ +%{python_wheeldir}/%{python_wheelname} %changelog -* Wed Nov 05 2025 Yu Peng - 25.3-1 -- Upgrade to 25.3 - * Remove support for the legacy setup.py develop editable method in setuptools editable installs - * Remove the deprecated --global-option and --build-option. --config-setting is now the only way to pass options to the build backend. - * Deprecate the PIP_CONSTRAINT environment variable for specifying build constraints. - * Permit spaces between a filepath and extras in an install requirement. - * Ensure the self-check files in the cache have the same permissions as the rest of the cache. - * Wed Sep 10 2025 xiangyuning - 23.3.1-5 - Fix CVE-2025-50181 diff --git a/remove-existing-dist-only-if-path-conflicts.patch b/remove-existing-dist-only-if-path-conflicts.patch new file mode 100644 index 0000000000000000000000000000000000000000..3a9ea2590ad04677710c92d1494df0a0a6d66c76 --- /dev/null +++ b/remove-existing-dist-only-if-path-conflicts.patch @@ -0,0 +1,115 @@ +From 2c3f3a590ddfc151a456b44a5f96f0f603d178e9 Mon Sep 17 00:00:00 2001 +From: Lumir Balhar +Date: Wed, 16 Feb 2022 08:36:21 +0100 +Subject: [PATCH] Prevent removing of the system packages installed under + /usr/lib when pip install --upgrade is executed. +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Resolves: rhbz#1550368 + +Co-Authored-By: Michal Cyprian +Co-Authored-By: Victor Stinner +Co-Authored-By: Petr Viktorin +Co-Authored-By: Lumir Balhar +Co-Authored-By: Miro HronĨok +Co-Authored-By: Karolina Surma +--- + src/pip/_internal/metadata/base.py | 12 +++++++++++- + src/pip/_internal/req/req_install.py | 2 +- + src/pip/_internal/resolution/legacy/resolver.py | 4 +++- + src/pip/_internal/resolution/resolvelib/factory.py | 12 ++++++++++++ + 4 files changed, 27 insertions(+), 3 deletions(-) + +diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py +index 151fd6d..f9109cd 100644 +--- a/src/pip/_internal/metadata/base.py ++++ b/src/pip/_internal/metadata/base.py +@@ -28,7 +28,7 @@ from pip._vendor.packaging.utils import NormalizedName + from pip._vendor.packaging.version import LegacyVersion, Version + + from pip._internal.exceptions import NoneMetadataError +-from pip._internal.locations import site_packages, user_site ++from pip._internal.locations import get_scheme, site_packages, user_site + from pip._internal.models.direct_url import ( + DIRECT_URL_METADATA_NAME, + DirectUrl, +@@ -560,6 +560,16 @@ class BaseDistribution(Protocol): + for extra in self._iter_egg_info_extras(): + metadata["Provides-Extra"] = extra + ++ @property ++ def in_install_path(self) -> bool: ++ """ ++ Return True if given Distribution is installed in ++ path matching distutils_scheme layout. ++ """ ++ norm_path = normalize_path(self.installed_location) ++ return norm_path.startswith(normalize_path( ++ get_scheme("").purelib.split('python')[0])) ++ + + class BaseEnvironment: + """An environment containing distributions to introspect.""" +diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py +index a1e376c..ed7facf 100644 +--- a/src/pip/_internal/req/req_install.py ++++ b/src/pip/_internal/req/req_install.py +@@ -416,7 +416,7 @@ class InstallRequirement: + f"lack sys.path precedence to {existing_dist.raw_name} " + f"in {existing_dist.location}" + ) +- else: ++ elif existing_dist.in_install_path: + self.should_reinstall = True + else: + if self.editable: +diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py +index fb49d41..040f2c1 100644 +--- a/src/pip/_internal/resolution/legacy/resolver.py ++++ b/src/pip/_internal/resolution/legacy/resolver.py +@@ -325,7 +325,9 @@ class Resolver(BaseResolver): + """ + # Don't uninstall the conflict if doing a user install and the + # conflict is not a user install. +- if not self.use_user_site or req.satisfied_by.in_usersite: ++ if ((not self.use_user_site ++ or req.satisfied_by.in_usersite) ++ and req.satisfied_by.in_install_path): + req.should_reinstall = True + req.satisfied_by = None + +diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py +index a4c24b5..e7e2da9 100644 +--- a/src/pip/_internal/resolution/resolvelib/factory.py ++++ b/src/pip/_internal/resolution/resolvelib/factory.py +@@ -1,6 +1,8 @@ + import contextlib + import functools + import logging ++import sys ++import sysconfig + from typing import ( + TYPE_CHECKING, + Dict, +@@ -549,6 +551,16 @@ class Factory: + if dist is None: # Not installed, no uninstallation required. + return None + ++ # Prevent uninstalling packages from /usr ++ try: ++ if dist.installed_location in ( ++ sysconfig.get_path('purelib', scheme='posix_prefix', vars={'base': sys.base_prefix}), ++ sysconfig.get_path('platlib', scheme='posix_prefix', vars={'platbase': sys.base_prefix}), ++ ): ++ return None ++ except KeyError: # this Python doesn't have 'rpm_prefix' scheme yet ++ pass ++ + # We're installing into global site. The current installation must + # be uninstalled, no matter it's in global or user site, because the + # user site installation has precedence over global. +-- +2.35.3 +