GeezerVison is showing github.com — Hosted requirements.txt rewrite skips PEP 440-equivalent pins like `six==1.16` for an installed 1.16.0, so `scan` exits 0 and pip installs the unpatched release (regression from v4.0.0) · Issue #475 · SocketDev/socket-patch
Hosted requirements.txt rewrite skips PEP 440-equivalent pins like six==1.16 for an installed 1.16.0, so scan exits 0 and pip installs the unpatched release (regression from v4.0.0) #475Description[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
The hosted requirements.txt rewriter compares the pinned version to the patch version as a raw string. pip compares them under PEP 440, where trailing zeros, leading zeros and case don't matter. As a result, a pin that pip resolves to exactly the patched release is treated as "no entry":
| requirements.txt |
what pip installs |
hosted rewrite on main |
six==1.16.0 |
six 1.16.0 |
rewritten to six @ <hosted wheel> --hash=… ✅ |
six==1.16 |
six 1.16.0 |
left alone, redirect_requirements_entry_not_found |
six==1.16.0.0 |
six 1.16.0 |
left alone, redirect_requirements_entry_not_found |
Six==01.16.0 |
six 1.16.0 |
left alone, redirect_requirements_entry_not_found |
scan --mode hosted (and get <uuid> --mode hosted) still finds the patch for the installed pkg:pypi/[email protected]. It then reports "redirected": 0 with that warning and exits 0 with status: success. The next pip install -r requirements.txt installs the unpatched upstream 1.16.0.
Short pins like ==X.Y for an X.Y.0 release are common in hand-written requirements files (for example urllib3==2.0, numpy==1.26).
The released v4.0.0 rewrites six==1.16 correctly, so this is a regression.
Impact
- The project is silently left unpatched. Exit 0 with
status: success, and the only signal is a warning that says there is no requirements.txt entry for six, even though there is one.
vex correctly attests nothing, so nothing downstream flags it either.
- The vendored path (
vendor / scan --mode vendored) has the same string comparison. It fails closed rather than silently: pypi_requirement_not_pinned: requirements.txt: six is not pinned to ==1.16.0, even though it is pinned to 1.16.0 under PEP 440. That's the "refusal fires when it shouldn't" case.
Repro (Linux, real pip)
The repro uses a local mock of the patch API (batch, by-package and package routes) that grants a hosted wheel for pkg:pypi/[email protected]. The probe workflow linked below contains a self-contained copy.
python3 -m venv .venv && .venv/bin/pip install six==1.16.0
printf 'six==1.16\n' > requirements.txt
socket-patch scan --mode hosted --yes --json \
--api-url http://127.0.0.1:$PORT --org test-org --patch-server-url http://127.0.0.1:$PORT
# → "status": "success", exit 0
# "redirect": {"redirected": 0, "warnings": [{"code": "redirect_requirements_entry_not_found",
# "detail": "no requirements.txt entry for [email protected]"}]}
cat requirements.txt # → six==1.16 (unchanged)
python3 -m venv fresh && fresh/bin/pip install -r requirements.txt
fresh/bin/python -c 'import six; print(getattr(six, "SOCKET_PATCHED", 0))' # → 0 (upstream 1.16.0)
# control: the same project with `six==1.16.0` is rewritten and the fresh install is PATCHED.
This reproduced on every run (local Linux pip 20.3.4 / 23.3.2 / 24.0, plus all CI cells below).
Expected vs actual
- Expected: CLI_CONTRACT.md (hosted mode) says scan should "rewrite ONLY the patched dependencies' lockfile / registry-config entries to point at the hosted packages". The requirement
six==1.16 is the patched dependency: pip's resolver picks exactly six 1.16.0 for it, the release the patch targets. docs/ecosystems.md says only "Version/source ambiguity is refused", and ==1.16 is not ambiguous under PEP 440, because zero-padding makes it equal to 1.16.0 and nothing else. The pin should be rewritten. Failing that, the run should at least fail or say the pin was skipped, rather than claim there is no entry and exit 0.
- Actual: the rewrite is silently skipped, the run exits 0, and the project installs unpatched bytes.
OS × pip matrix (probe run 36878654662)
| OS |
Python |
pip |
==1.16.0 (control) |
==1.16 |
==1.16.0.0 |
Six==01.16.0 |
| ubuntu-latest |
3.8 |
20.3.4, 23.0.1 |
PATCHED |
❌ UNPATCHED |
❌ |
❌ |
| ubuntu-latest |
3.13 |
24.0, bundled |
PATCHED |
❌ |
❌ |
❌ |
| macos-latest |
3.8 |
20.3.4, 23.0.1, bundled |
PATCHED |
❌ |
❌ |
❌ |
| macos-latest |
3.13 |
24.0, 26.2.1 |
PATCHED |
❌ |
❌ |
❌ |
| windows-latest |
3.8 |
20.3.4, 23.0.1, 21.1.1 (bundled) |
PATCHED |
❌ |
❌ |
❌ |
| Linux (local) |
3.10 / 3.12 |
20.3.4, 23.3.2, 24.0 |
PATCHED |
❌ |
❌ |
❌ |
First bad commit
git bisect with the repro above: ade011e (#238) is good, and 745e8dc "Harden uv lockfile patch preservation and verify every uv release family" (#239) is the first bad commit. The published v4.0.0 (96df6ae) matched requirements lines by name only and rewrites six==1.16.
Suspect code
crates/socket-patch-core/src/patch/redirect/requirements.rs:248: RequirementVersion::Exact(version) if version != dep.version => continue is a raw string comparison. A PEP 440 comparison (pad release segments with zeros, strip leading zeros, normalise case and spelling) would match pip.
crates/socket-patch-core/src/vendor/pypi_requirements.rs:81: the vendored spec_no_ws == format!("=={version}") has the same raw comparison, which produces the false pypi_requirement_not_pinned refusal.
- The lock-only path (
utils/requirements.rs::exact_pin) carries the spelled version through to the purl (pkg:pypi/[email protected] in the batch request). It may need the same normalisation when no venv is present.
Activitymikolalysenko commented on Oct 1, 2026 CollaboratorAuthor More actions [agent] Triaged as priority:p1 (pip). Confirmed on main 6e7ef74: patch/redirect/requirements.rs compares RequirementVersion::Exact(version) != dep.version as raw strings, and vendor/pypi_requirements.rs compares spec_no_ws == format!("=={version}"), so PEP 440-equivalent pins (==1.16, ==1.16.0.0, ==01.16.0) miss. No duplicate or open fix PR found.
Generated by Claude Code mikolalysenko commented on Oct 1, 2026 CollaboratorAuthor More actions [agent] Claiming this issue (shared root cause: requirements.txt pin matching compares versions as raw strings instead of under PEP 440). Branch: agent/fix-requirements-pep440-pin-match. Claim-ID: 2026-10-01T15:20:44Z-19ebc3
Generated by Claude Code mikolalysenko commented on Oct 1, 2026 CollaboratorAuthor More actions added 2 commits that reference this issue on Oct 1, 2026This issue will close once commit eea2f62 is merged into the 'main' branch.eea2f62 This issue will close once commit bcc5335 is merged into the 'main' branch.bcc5335 mikolalysenko commented on Oct 1, 2026 CollaboratorAuthor More actions [agent] #478 is ready for review. It fixes the hosted rewrite, the false vendored pypi_requirement_not_pinned refusal, and the same raw comparison in the Hatch rewriter. Follow-up not in #478: on a lock-only checkout with no venv, exact_pin still carries the spelled version into the purl (pkg:pypi/[email protected]). Matching that to the 1.16.0 release needs PyPI's canonical release spelling, so it's left for a separate change. Details are in the PR's "Notes / follow-ups".
Generated by Claude Code
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|