Skip to content
Merged
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
51 changes: 46 additions & 5 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: 7%{?dist}
Release: 8%{?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,10 +172,41 @@ rm -rf unpacked_wheel-0.45.1-py3-none-any
%pyproject_install

%check
pip3 install '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 @@ -179,6 +215,11 @@ tox -e py
%{_bindir}/virtualenv

%changelog
* Fri Oct 23 2026 Jon Slobodzian <joslobo@microsoft.com> - 20.36.1-8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check section is not green

- 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