[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).
[agent] Found by the scheduled Pipenv bug-hunt routine (ledger #313).
Summary
A Pipenv project often has a root
requirements.txtexported bypipenv requirements(orpipenv lock -ron 2018) for Docker or plain-pip installs. Hosted mode rewires both files; that's been the case since the #333 fix. Vendored mode rewires onlyPipfile.lock:scan --mode vendored/vendorleavessix==1.16.0inrequirements.txtuntouched. It reportsstatus: success, exit 0, and emits no warning.vendor --checkthen reportsverified: 1, success.pip install -r requirements.txtinstalls the unpatched upstream six.vexsees the conflict and refuses to attest (patched_ref_unattributable, exit 1). Its remedy says "rewire both locks (re-runsocket-patch scan/vendor)", but re-running givesalready_vendoredand changes nothing. So the user can't get the project attested.scan --mode vendoredreportsvendor_takeover_reverted_redirect"restored its upstream registry entry (Pipfile.lock, requirements.txt)", then vendors only Pipfile.lock. So the mode switch turns a patchedrequirements.txtback intosix==1.16.0from PyPI, and still reports success.The vendored flavor router
detect_pypi_flavor(crates/socket-patch-core/src/vendor/pypi.rs:248) returnsPypiFlavor::Pipenvas soon asPipfile.lockexists (:333).requirements.txtonly counts as a competing install source on the standalone-pylock branch (:286), so thepypi_multiple_lockfileswarning (:312) never fires for Pipfile.lock + requirements.txt.Impact
Teams that build containers with
pipenv requirements > requirements.txt && pip install -r requirements.txtship the unpatched package after a successful vendored run, andvendor --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.0wheel)Every cell below was reproduced twice.
Expected vs actual
requirements.txtvendoring is supported (docs/ecosystems.md: "pipenv … and requirements.txt"), and works on the same file without a Pipfile.lock.pypi_multiple_lockfiles… a sibling lockfile of another package manager will still install UNPATCHED bytes; names the wired winner + the ignored locks". Thedetect_pypi_flavordoc comment promises this "LOUD" warning, because such files otherwise "go stale-but-valid, which is otherwise invisible".vendor --checkgreen, the pip path unpatched. The takeover actively unpatchesrequirements.txt, and the VEX remedy text can't be followed.OS × version (Linux, main
045d7ec)vendor --checkgreenpip install -runpatchedlock -r)Control: the same
requirements.txtwith 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:248detect_pypi_flavor: aPipfile.lock(likewisepoetry.lock/pdm.lock, untested here) wins without countingrequirements.txtas a competing install source (:286is the only place it's added topresent).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,
-rinclude), #333 (hosted, closed), #328 / #503 (vendored → hosted takeover, which does rewire both files).