Skip to content

Vendored mode in a Pipenv project wires only Pipfile.lock and silently leaves a sibling requirements.txt unpatched, and the hosted → vendored takeover reverts that file's hosted pin to plain PyPI #612

Description

[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).

Summary

A Pipenv project often has a root requirements.txt exported by pipenv requirements (or pipenv lock -r on 2018) for Docker or plain-pip installs. Hosted mode rewires both files; that's been the case since the #333 fix. Vendored mode rewires only Pipfile.lock:

  • scan --mode vendored / vendor leaves six==1.16.0 in requirements.txt untouched. It reports status: success, exit 0, and emits no warning. vendor --check then reports verified: 1, success.
  • pip install -r requirements.txt installs the unpatched upstream six.
  • vex sees the conflict and refuses to attest (patched_ref_unattributable, exit 1). Its remedy says "rewire both locks (re-run socket-patch scan / vendor)", but re-running gives already_vendored and changes nothing. So the user can't get the project attested.
  • The hosted → vendored takeover is worse. Start from a hosted project where both files carry the hosted URL. scan --mode vendored reports vendor_takeover_reverted_redirect "restored its upstream registry entry (Pipfile.lock, requirements.txt)", then vendors only Pipfile.lock. So the mode switch turns a patched requirements.txt back into six==1.16.0 from PyPI, and still reports success.

The vendored flavor router detect_pypi_flavor (crates/socket-patch-core/src/vendor/pypi.rs:248) returns PypiFlavor::Pipenv as soon as Pipfile.lock exists (:333). requirements.txt only counts as a competing install source on the standalone-pylock branch (:286), so the pypi_multiple_lockfiles warning (:312) never fires for Pipfile.lock + requirements.txt.

Impact

Teams that build containers with pipenv requirements > requirements.txt && pip install -r requirements.txt ship the unpatched package after a successful vendored run, and vendor --check (the CI gate) passes. A user switching an existing hosted project to vendored loses the patch on the pip path without any signal except a later VEX refusal.

Repro (Linux; real Pipenv; local mock patch API serving a patched six 1.16.0 wheel)

mkdir app && cd app
cat > Pipfile <<'EOF'
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
six = "==1.16.0"
idna = "==3.7"

[requires]
python_version = "3.12"
EOF
pipenv lock
pipenv requirements > requirements.txt        # 2018.11.26: pipenv lock -r > requirements.txt

socket-patch scan --mode vendored --yes --json   # status success, exit 0, no warning
grep six requirements.txt                        # six==1.16.0; python_version >= ...   (unchanged)
grep -c socket/vendor Pipfile.lock               # 1
socket-patch vendor --check --json               # success, verified: 1
uv venv rv && uv pip install -p rv/bin/python -r requirements.txt
rv/bin/python -c "import six; print(getattr(six,'SOCKET_PATCHED','UNPATCHED'))"   # UNPATCHED
pipenv sync && pipenv run python -c "import six; print(six.SOCKET_PATCHED)"       # 1
socket-patch vex --product pkg:pypi/app@1.0.0    # exit 1: patched_ref_unattributable, "re-run scan / vendor"
socket-patch scan --mode vendored --yes --json   # already_vendored; requirements.txt still unpatched

# Takeover variant (fresh copy of the same project)
socket-patch scan --mode hosted --yes --json     # rewrittenFiles: [Pipfile.lock, requirements.txt]
socket-patch scan --mode vendored --yes --json   # success; vendor_takeover_reverted_redirect (Pipfile.lock, requirements.txt)
cat requirements.txt                             # six==1.16.0 ; python_version ...   <- hosted pin gone

Every cell below was reproduced twice.

Expected vs actual

  • Expected: one of these:
    • Vendored wires every install source that pins the package, as hosted does. Plain requirements.txt vendoring is supported (docs/ecosystems.md: "pipenv … and requirements.txt"), and works on the same file without a Pipfile.lock.
    • Or, at minimum, the documented warning. CLI_CONTRACT.md:1179: "pypi_multiple_lockfiles … a sibling lockfile of another package manager will still install UNPATCHED bytes; names the wired winner + the ignored locks". The detect_pypi_flavor doc comment promises this "LOUD" warning, because such files otherwise "go stale-but-valid, which is otherwise invisible".
    • A takeover should never leave a file less patched than it found it.
  • Actual: silent success, vendor --check green, the pip path unpatched. The takeover actively unpatches requirements.txt, and the VEX remedy text can't be followed.

OS × version (Linux, main 045d7ec)

Pipenv vendored skips requirements.txt, no warning vendor --check green pip install -r unpatched vex refuses hosted → vendored unpatches requirements.txt
2018.11.26 (py3.8, lock -r) ✅ repro 2/2 ✅ ✅ ✅ not run
2023.12.1 (py3.12) ✅ repro 2/2 ✅ ✅ ✅ not run
2026.8.0 (py3.12) ✅ repro 2/2 ✅ ✅ ✅ ✅ repro
macOS / Windows not probed (pure routing logic, no path handling)

Control: the same requirements.txt with no Pipfile.lock beside it is vendored correctly (./.socket/vendor/pypi/<uuid>/six-….whl ; <markers> # socket-patch vendor: six==1.16.0).

First bad version

Not bisected. v4.0.0 can't vendor against the mock (vendor_fetch_unverifiable), so I couldn't compare it. The routing precedence predates the #503 / #572 changes.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi.rs:248 detect_pypi_flavor: a Pipfile.lock (likewise poetry.lock / pdm.lock, untested here) wins without counting requirements.txt as a competing install source (:286 is the only place it's added to present).
  • crates/socket-patch-cli/src/commands/vendor.rs:2527: the takeover reverts the redirect in every file it touched, before the flavor router decides which single file to vendor.

Related, but different: #567 (hosted, -r include), #333 (hosted, closed), #328 / #503 (vendored → hosted takeover, which does rewire both files).

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