Skip to content

A requirements.txt pin spelled ===X, ==X.0 or ==vX doesn't contest another lock's Socket wiring of release X, so vex attests not_affected while pip install -r installs the upstream release (#1549 residual) #1602

Open

Description

[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).

Summary

Since #1562 (fix for #1549), a requirements.txt line that installs the same release from somewhere other than Socket contests another lock's Socket wiring of that release. For example, a Pipfile.lock wired hosted or vendored beside a requirements.txt that says vexdemo==1.2.3 makes vex withhold the statement with patched_ref_unattributable ("… but requirements.txt resolves the same version").

That only works for the canonical name==X spelling. pip installs exactly the same upstream release for these lines, but none of them contests the wiring:

requirements.txt line (wired release 1.2.3) pip installs vex today
vexdemo==1.2.3 (control) upstream 1.2.3 contested, exit ≠ 0 ✅
vexdemo===1.2.3 (arbitrary equality) upstream 1.2.3 not_affected, exit 0
vexdemo==1.2.3.0 (PEP 440-equal; also six==1.16 for 1.16.0) upstream 1.2.3 not_affected, exit 0
vexdemo==v1.2.3 (PEP 440 v prefix) upstream 1.2.3 not_affected, exit 0

The same holds for single-version ranges that pip resolves to the wired release, such as ~=1.2.3, ==1.2.* and >=1.2.3,<=1.2.3. Those depend on what PyPI holds, so they may be a design call. The three exact spellings above are not: they can only install release X.

Impact

vex emits a not_affected statement (exit 0, with or without --no-verify), and vendor --check presumably stays green as well, for a project whose pip install -r requirements.txt ships the unpatched upstream wheel. That's the #1549 harm, reached through a different spelling. A hand-written six==1.16 (two segments) for release 1.16.0 is a common real-world form; socket-patch already treats it as equal to 1.16.0 everywhere else (#475, #604).

Repro

This uses the repo's own #1549 harness (crates/socket-patch-cli/tests/vex_pipenv_pip_common, the real CLI binary). Add this throwaway test to crates/socket-patch-cli/tests/e2e_vex_lockfile/pipenv.rs:

#[test]
#[ignore]
fn requirements_spellings_contest() {
    for (label, other) in [
        ("control", format!("{NAME}=={VERSION}\n")),
        ("===", format!("{NAME}==={VERSION}\n")),
        ("==X.0", format!("{NAME}==1.2.3.0\n")),
        ("==vX", format!("{NAME}==v{VERSION}\n")),
    ] {
        let r = std::panic::catch_unwind(|| {
            contested_by_a_non_socket_direct_reference(
                &flavors()[..1], "requirements.txt", &[(label, other.clone())]);
        });
        eprintln!("{label}: {}", if r.is_ok() { "contested" } else { "NOT contested" });
    }
}
cargo test -p socket-patch-cli --test e2e_vex_lockfile requirements_spellings_contest -- --ignored --nocapture
control: contested
===: NOT contested      # vex exit 0, "status": "not_affected" for pkg:pypi/vexdemo@1.2.3
==X.0: NOT contested
==vX: NOT contested

pip installs the upstream release for each spelling (real pip, six from PyPI):

for line in 'six===1.16.0' 'six==1.16.0.0' 'six==v1.16.0' 'six==1.16'; do
  echo "$line" > r.txt; python -m pip install --dry-run --no-deps -r r.txt   # pip 20.3.4: pip download --no-deps -r r.txt
done
# every cell → six-1.16.0-py2.py3-none-any.whl from files.pythonhosted.org

Expected vs actual

  • Expected: CLI_CONTRACT.md, "Contested locks": a lock that "resolves the same name@version from a non-Socket source" drops the reference with patched_ref_unattributable. pip resolves ===1.2.3, ==1.2.3.0 and ==v1.2.3 to the same name@version as ==1.2.3, so each should contest the wiring the same way the canonical pin does.
  • Actual: those lines are skipped as evidence. vex attests not_affected with exit 0.

OS × version

OS pip / Python reproduces
Linux real CLI harness (Pipfile.lock wired, hosted and vendored cells) yes, twice
Linux pip 20.3.4 / py3.8, 24.0 / py3.11, 26.2.1 / py3.13 install upstream 1.16.0 for all four spellings yes
macOS / Windows not run (probe branches are blocked for this routine) n/a; the code path is OS-independent

First bad: the contest itself is new in 0d741d8 (#1562), so this is a residual of that fix and not a regression. Tested on main 657fce2.

Suspect code

  • crates/socket-patch-core/src/vex/discover/pypi_other.rs:347: registry_pin reads the line through utils::requirements::exact_pin (crates/socket-patch-core/src/utils/requirements.rs:542). That function returns None for === and for a version that doesn't start with a digit (v1.2.3), and it returns 1.2.3.0 verbatim.
  • crates/socket-patch-core/src/vex/discover/mod.rs:924: resolved_elsewhere stores canonical_base_purl(purl), and the contest compares purl strings, so pkg:pypi/vexdemo@1.2.3.0 never matches @1.2.3. The hosted rewriter already compares with utils::pep440::versions_equal (patch/redirect/requirements.rs); the contest needs the same PEP 440 equality, plus === (which pip compares as a string).

Related: #1549 / #1562 (the original contest), #1587 / #1599 (bare wheel paths, a separate residual), #475 / #604 (PEP 440-equal pins in the rewriter and in lock-only discovery).

Activity

  1. added
    bugSomething isn't working
    bughuntFound by a scheduled package-manager bug-hunt agent
    pm:pippip / requirements.txt
    on Oct 16, 2026
  2. mikolalysenko commented on Oct 16, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (pip / requirements.txt, false not_affected). Confirmed on main 657fce2: utils::requirements::exact_pin returns None for === and for a version not starting with a digit, and returns 1.2.3.0 verbatim, and the contest compares canonical purl strings rather than PEP 440 versions. Residual of #1549 / #1562. Separate from #1587 / #1599 (wheel-path lines), which goes through requirement_location, not exact_pin. No open PR covers it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:pippip / requirements.txtpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions