Skip to content

Lockfile discovery treats a requirements.txt -r include as a competing lock, so after a hosted rewrite vex attests nothing (exit 2) and rollback refuses (exit 1), although pip install -r requirements.txt installs the patched wheel #1086

Description

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

Summary

Take a pip project where the root requirements.txt pins six==1.16.0 and also includes -r dev.txt, and dev.txt pins six==1.16.0 too. Duplicate pins like this are common with split base/dev files. scan --mode hosted rewrites the root line to six @ <socket-hosted wheel>#sha256=… and leaves dev.txt alone, because hosted only rewrites the root file. It exits 0 with status: success and redirected: 1.

pip reads the root file and its includes as one requirement set. A direct URL plus a compatible ==1.16.0 specifier resolves to the URL, so pip install -r requirements.txt installs the patched wheel on pip 20.3.4, 24.0 and 26.2.1.

Lockfile discovery, however, treats dev.txt as a separate lock that "resolves the same version from elsewhere" (contest_across_locks). So:

  • vex drops the ref with patched_ref_unattributable, then fails with Error: Manifest not found … no hosted or vendored patch references were found … nothing to attest (exit 2). The error message is also misleading, since the reference is there.
  • rollback refuses the whole project: requirements.txt wire(s) Socket-hosted patches that cannot be attributed to one package version (the lockfiles disagree …), status: error, exit 1. The hosted pin can't be unpatched with socket-patch.
  • Re-running scan --mode hosted, the remedy the warning names, exits 0 with success and no warning, and changes nothing.

Impact

  • CI that gates on vex fails for a project whose pip install -r requirements.txt is actually patched.
  • rollback / remove can't undo a pin that scan wrote a moment earlier. The only way out is git checkout.
  • Open PR Decide whether a hosted patch is pinned through lockfile discovery alone #1058 adds a pre-write attribution gate built on this same discovery. With it, the hosted scan would skip this candidate as redirect_unattributable, and the project would never get patched. Fixing the contest rule first would avoid that.

Repro (Linux, main 05ecc6e, local mock patch API serving a patched six-1.16.0 wheel)

SP=target/release/socket-patch
API="--api-url $MOCK --org test-org --api-token fake --patch-server-url $MOCK"
mkdir proj && cd proj
python3.11 -m venv .venv && .venv/bin/pip install -q pip==26.2.1 six==1.16.0
printf -- '-r dev.txt\nsix==1.16.0\n' > requirements.txt
printf 'six==1.16.0\n' > dev.txt
$SP scan --yes --json --mode hosted --ecosystems pypi $API     # exit 0, success, redirected 1, warnings: [redirect_pypi_stale_install]
.venv/bin/pip install --force-reinstall -r requirements.txt    # installs the Socket wheel; `import six` -> patched
$SP vex $API --product pkg:pypi/app@1.0.0 -O vex.json          # exit 2:
#   Warning: requirements.txt: pkg:pypi/six@1.16.0 is wired to Socket patch <uuid>, but dev.txt resolves the
#            same version from elsewhere (not a Socket patch) …
#   Error: Manifest not found at ./.socket/manifest.json, and no hosted or vendored patch references were found …
$SP rollback --yes --json $API                                 # exit 1, status error, "cannot be attributed to one package version"
$SP scan --yes --json --mode hosted --ecosystems pypi $API     # exit 0, success, no warning; state unchanged

The include line can come first or last (six==1.16.0\n-r dev.txt\n); both fail the same way.

Expected vs actual

  • Expected: CLI_CONTRACT.md line 370 lists the PyPI wiring as "requirements.txt + its in-root -r includes", which is one install tree. The "Contested locks" rule (line 382) is about another lock resolving the same name@version, where which lock the build uses "depends on the package manager that runs". Here there's only one package manager and one install (pip install -r requirements.txt), and it installs the patched wheel. The module docs in vex/discover/pypi_other.rs (lines 53–61) already treat the root and its includes as one tree read by the planner's own walk. vex should attest not_affected, and rollback should restore the root line. The include's six==1.16.0 should either not count as a contest within the same tree, or count only when it pins a version the URL doesn't satisfy.
  • Actual: each include is a separate "lock" in contest_across_locks, so a compatible duplicate pin vetoes the root's wiring. vex exits 2, rollback exits 1, and the re-scan remedy is a no-op.

OS × version

Cell Result
Linux, pip 26.2.1 / py3.11, include before the root pin fail (3/3: vex exit 2, rollback exit 1)
Linux, pip 26.2.1 / py3.11, include after the root pin fail (1/1)
Linux, pip 20.3.4 / py3.8 and 24.0 / py3.11: pip install -r requirements.txt from a fresh venv patched wheel installed (so the project is patched)
Control: no duplicate pin pass (not_affected)
Control: duplicate pin in a sibling requirements-dev.txt the root never includes pass (not_affected; siblings aren't read, by design)
Control: -c constraints.txt pinning six==1.16.0 pass (not_affected)
Control: the include has a range (six>=1.0), not an exact pin pass (not_affected)

macOS / Windows: not probed. The logic is path-only and OS-independent.

First bad: this is not a regression from this week. Main 9c43dfc (2026-10-05) behaves the same (vex exit 2, rollback exit 1). v4.0.0 has no lockfile-based hosted vex / rollback to compare against.

Suspect code

  • crates/socket-patch-core/src/vex/discover/pypi_other.rs:257: every exact registry pin in every file of the include tree is recorded as resolved_elsewhere(file, …) under its own file name.
  • crates/socket-patch-core/src/vex/discover/mod.rs:698-711 (contest_across_locks): a ref is contested when e.file != r.source_file, so a root ref and an include pin of the same tree contest each other. Files of one requirements include tree should share a lock identity here, the way the planner and discovery already walk them as one.

Related, but different: #567 (Pipenv: Pipfile.lock vs a requirements include; those really are two package managers), #410 (all-hosted rollback, fixed), and PR #1058 (the attribution gate that would turn this into a skipped patch).

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions