Skip to content
Open
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
62 changes: 50 additions & 12 deletions SPECS/python-virtualenv/python-virtualenv.spec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Summary: Virtual Python Environment builder
Name: python-virtualenv
Version: 20.36.1
Release: 8%{?dist}
Release: 9%{?dist}
License: MIT
Vendor: Microsoft Corporation
Distribution: Azure Linux
Expand All @@ -24,6 +24,7 @@ Patch1011: CVE-2026-13346v0.patch
Patch1012: CVE-2026-13346v1.patch
Patch1013: CVE-2026-102925.patch
Patch1014: CVE-2026-102930.patch
Patch1015: remove-compromised-wheel-ca-roots.patch
BuildArch: noarch

%description
Expand Down Expand Up @@ -58,6 +59,7 @@ virtualenv is a tool to create isolated Python environment.
%patch -P 0 -p1
%patch -P 1013 -p1
%patch -P 1014 -p1
%patch -P 1015 -p1

# Manual patching for CVE-2025-50181 and CVE-2026-1703v0
# For CVE-2025-50181, poolmanager.py file is located in 2 different places and each is of different version so the same patch cannot be applied to all of them.
Expand Down Expand Up @@ -158,6 +160,9 @@ zip -r ../src/virtualenv/seed/wheels/embed/unpacked_wheel-0.45.1-py3-none-any.wh
popd
rm -rf unpacked_wheel-0.45.1-py3-none-any

%{__python3} Tools/remove-untrusted-wheel-roots.py \
--expected-removals 3 src/virtualenv/seed/wheels/embed/pip-*.whl

%generate_buildrequires

%build
Expand All @@ -167,14 +172,41 @@ rm -rf unpacked_wheel-0.45.1-py3-none-any
%pyproject_install

%check
# Pin packaging to the RPM-installed version so pip does not try to
# upgrade it for virtualenv >= 21.14.6 (requires packaging >= 26.3); the
# rpm-installed copy has no RECORD file, so pip aborts with
# uninstall-no-record-file and the test tooling is never installed.
pip3 install packaging==23.2 'tox>=3.27.1,<4.0.0'
# skip "test_can_build_c_extensions" tests since they fail on python3_version >= 3.12. See https://src.fedoraproject.org/rpms/python-virtualenv/blob/rawhide/f/python-virtualenv.spec#_153
sed -i 's/coverage run -m pytest {posargs:--junitxml {toxworkdir}\/junit\.{envname}\.xml tests --int}/coverage run -m pytest {posargs:--junitxml {toxworkdir}\/junit\.{envname}\.xml tests -k "not test_can_build_c_extensions" --int}/g' tox.ini
tox -e py
# Keep the test environment outside zipapp's source copy.
check_env=$(mktemp -d "$PWD/../virtualenv-check.XXXXXX")
python3 -m venv "$check_env"
cat > "$check_env/constraints.txt" <<'EOF'
covdefaults==2.3.0
coverage==7.10.6
coverage-enable-subprocess==1.0
distlib==0.3.8
filelock==3.20.3
flaky==3.8.1
iniconfig==2.1.0
packaging==25.0
platformdirs==4.3.8
pluggy==1.6.0
pygments==2.19.2
pytest==8.4.2
pytest-env==1.1.5
pytest-mock==3.15.1
pytest-randomly==3.16.0
pytest-timeout==2.4.0
python-dateutil==2.9.0.post0
setuptools==80.9.0
six==1.17.0
time-machine==2.19.0
typing-extensions==4.13.2
EOF
set -- %{_pyproject_wheeldir}/*.whl
test "$#" -eq 1
"$check_env/bin/python" -m pip install --disable-pip-version-check \
--constraint "$check_env/constraints.txt" "$1[test]"
export PATH="$check_env/bin:$PATH"
unset COVERAGE_PROCESS_START
export PYTHONWARNDEFAULTENCODING=1
"$check_env/bin/python" -m pytest --junitxml junit.py.xml \
tests -k 'not test_can_build_c_extensions' --int

%files -n python3-virtualenv
%defattr(-,root,root,-)
Expand All @@ -183,9 +215,15 @@ tox -e py
%{_bindir}/virtualenv

%changelog
* Mon Oct 12 2026 Kanishk Bansal <kanbansal@microsoft.com> - 20.36.1-8
- Pin packaging==23.2 when pip-installing test tooling in %%check to fix
ptest failure (uninstall-no-record-file) caused by virtualenv 21.14.6.
* Fri Oct 30 2026 Kanishk Bansal <kanbansal@microsoft.com> - 20.36.1-9
- Preserve the dev fix for RPM-owned packaging during ptest bootstrap.
- Use the published private, pinned test environment in place of the
system-pip packaging==23.2/tox bootstrap.

* Fri Oct 23 2026 Jon Slobodzian <joslobo@microsoft.com> - 20.36.1-8
- Remove affected eMudhra and AAA roots from both embedded pip wheels.
- Regenerate wheel RECORD entries after the existing security backports.
- Run pytest directly with pinned test dependencies in a private environment.

* Mon Oct 05 2026 Kshitiz Godara <kgodara@microsoft.com> - 20.36.1-7
- Patch for CVE-2026-102925 and CVE-2026-102930
Expand Down
119 changes: 119 additions & 0 deletions SPECS/python-virtualenv/remove-compromised-wheel-ca-roots.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Subject: [PATCH] Distrust incident-related eMudhra and AAA trust anchors

Remove only the affected trust anchors, including the AAA cross-signing
path. Preserve the unaffected emSign G1, C1 and C3 anchors.
Reference: https://emudhra.we-have-a-problem.com/
Upstream-status: Azure Linux trust-store backport

--- /dev/null
+++ b/Tools/remove-untrusted-wheel-roots.py
@@ -0,0 +1,109 @@
+"""Remove incident-related trust anchors from bundled wheels and renew RECORD."""
+
+import argparse
+import base64
+import csv
+import hashlib
+import io
+from pathlib import Path
+import re
+import ssl
+import tempfile
+import zipfile
+
+
+UNTRUSTED_SHA256 = {
+ "d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4",
+ "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b",
+}
+CERTIFICATE = re.compile(
+ rb"-----BEGIN CERTIFICATE-----\s+[A-Za-z0-9+/=\s]+?-----END CERTIFICATE-----"
+)
+
+
+def prepare_wheel(path):
+ with zipfile.ZipFile(path) as archive:
+ infos = archive.infolist()
+ if len({info.filename for info in infos}) != len(infos):
+ raise ValueError(f"{path}: duplicate wheel members")
+ records = [
+ info.filename
+ for info in infos
+ if info.filename.endswith(".dist-info/RECORD")
+ ]
+ if len(records) != 1:
+ raise ValueError(f"{path}: expected exactly one wheel RECORD")
+ record = records[0]
+ if any(
+ name in archive.namelist() for name in (record + ".jws", record + ".p7s")
+ ):
+ raise ValueError(f"{path}: cannot rewrite a signed wheel")
+ payload = {info.filename: archive.read(info) for info in infos}
+
+ removed = 0
+
+ def retain_certificate(match):
+ nonlocal removed
+ der = ssl.PEM_cert_to_DER_cert(match.group().decode("ascii"))
+ if hashlib.sha256(der).hexdigest() in UNTRUSTED_SHA256:
+ removed += 1
+ return b""
+ return match.group()
+
+ for name, data in payload.items():
+ if name.endswith((".pem", ".crt")):
+ payload[name] = CERTIFICATE.sub(retain_certificate, data)
+
+ if not removed:
+ return None, 0
+
+ output = io.StringIO(newline="")
+ writer = csv.writer(output, lineterminator="\n")
+ for info in infos:
+ name = info.filename
+ if info.is_dir():
+ continue
+ if name == record:
+ writer.writerow((name, "", ""))
+ else:
+ data = payload[name]
+ digest = base64.urlsafe_b64encode(hashlib.sha256(data).digest())
+ writer.writerow((name, "sha256=" + digest.rstrip(b"=").decode(), len(data)))
+ payload[record] = output.getvalue().encode("utf-8")
+ return (path, infos, payload), removed
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--expected-removals", type=int, required=True)
+ parser.add_argument("wheels", nargs="+", type=Path)
+ args = parser.parse_args()
+ plans = []
+ total = 0
+ for path in args.wheels:
+ plan, removed = prepare_wheel(path)
+ if plan is not None:
+ plans.append(plan)
+ total += removed
+ print(f"{path}: removing {removed} affected trust anchors")
+ if total != args.expected_removals:
+ raise ValueError(
+ f"Expected {args.expected_removals} trust anchors, found {total}; "
+ "review the bundled wheels before changing this expectation"
+ )
+ for path, infos, payload in plans:
+ with tempfile.NamedTemporaryFile(
+ dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False
+ ) as temporary:
+ temporary_path = Path(temporary.name)
+ try:
+ with zipfile.ZipFile(temporary_path, "w") as archive:
+ for info in infos:
+ archive.writestr(info, payload[info.filename])
+ temporary_path.replace(path)
+ finally:
+ temporary_path.unlink(missing_ok=True)
+
+
+if __name__ == "__main__":
+ main()
Loading