Skip to content

Vendored scan of a Pipenv use_pylock = true project wires only pylock.toml, but Pipenv installs from Pipfile.lock, so pipenv sync / install --deploy install the unpatched release after a "success" run #1122

Description

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

Summary

With [pipenv] use_pylock = true in the Pipfile, Pipenv 2026's pipenv lock writes both Pipfile.lock and pylock.toml. The pylock.toml carries [tool.pipenv] generated_from = "Pipfile.lock". When both files are present, Pipenv installs from Pipfile.lock: pipenv sync and pipenv install --deploy take the bytes from Pipfile.lock and ignore pylock.toml. I verified this below on 2026.0.0, 2026.4.0 and 2026.8.0.

scan --mode vendored routes this layout to the standalone python-lock flavor, because detect_pypi_flavor step 2 (a standalone pylock*.toml that contains the package) ranks above step 5 (Pipfile.lock → pipenv). So it rewrites only pylock.toml (archive = { path = ".socket/vendor/pypi/<uuid>/…whl" }) and leaves Pipfile.lock on PyPI. The run reports status: success, exit 0, "Vendored 1 package". Its only warning is pypi_multiple_lockfiles: "wiring pylock.toml; installs driven by Pipfile.lock retain their existing sources".

So every Pipenv install afterwards puts the unpatched upstream release in place. Hosted mode handles the same layout correctly: it pins both files, and pipenv sync gives the patched bytes.

This is distinct from #912. #912 is the pylock-only checkout, where Pipenv reads pylock.toml and drops archive. #912 already notes that with both files present "Pipenv installs from Pipfile.lock", which is exactly why vendoring the pylock alone misses here.

Impact

  • The documented use_pylock = true layout, vendored, never gets the patch in any Pipenv install (CI, Docker, --deploy), although the scan says it vendored the package.
  • The user-visible remedy is awkward. vendor --check (exit 1) says to delete whichever lock the project doesn't install from, but pipenv lock regenerates pylock.toml while use_pylock = true is set, and the next vendored run wires it again.
  • vex refuses, as it should (pkg:pypi/six@1.16.0 is wired … but Pipfile.lock resolves the same version from elsewhere, exit 1). So there's no false attestation. The defect is that the wrong file is chosen as the governing lock.

Repro (Linux; real Pipenv 2026.8.0, py3.11; 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"

[requires]
python_version = "3.11"

[pipenv]
use_pylock = true
EOF
pipenv lock                                   # writes Pipfile.lock AND pylock.toml
socket-patch scan --mode vendored --yes       # exit 0, "Vendored 1 package."
                                              # Warning: wiring pylock.toml; installs driven by Pipfile.lock retain their existing sources
grep -c socket/vendor Pipfile.lock            # 0
grep -c socket/vendor pylock.toml             # 1
pipenv --rm; pipenv install --deploy
pipenv run python -c "import six; print('SOCKET-PATCHED' in open(six.__file__).read())"   # False
pipenv --rm; pipenv sync                      # same: False
socket-patch vendor --check                   # exit 1: wiring contested
socket-patch vex --offline --product pkg:pypi/app@1.0.0 -O vex.json   # exit 1, refuses

# Control: the same project, hosted
socket-patch scan --mode hosted --yes         # rewrittenFiles: [Pipfile.lock, pylock.toml]
pipenv --rm; pipenv sync                      # patched
# Which file Pipenv reads: hosted Pipfile.lock + pristine pylock.toml -> sync / --deploy give the PATCHED bytes

scan --mode vendored --dry-run previews the same pylock-only wiring. Reproduced 2/2 on 2026.8.0, plus once each on 2026.0.0 and 2026.4.0.

Expected vs actual

  • Expected: vendored wires the file the project's installer reads. When a Pipfile sits beside both locks (and pylock.toml says generated_from = "Pipfile.lock"), that's Pipfile.lock. The flavor router's own doc says it routes by "this tool manages installs". docs/ecosystems.md lists Pipenv Pipfile.lock vendoring as supported ("every Pipfile.lock category is rewired"). It would also be reasonable to wire both files, as hosted mode does. CLI_CONTRACT.md: "A dep counts as redirected only when its hosted-artifact URL … actually landed in a project file". The vendored analogue is a rewrite the installer honours.
  • Actual: only pylock.toml is wired, and Pipenv ignores it while Pipfile.lock exists. The scan succeeds, and every Pipenv install is unpatched.

OS × version

Pipenv both locks written by pipenv lock vendored wires pipenv sync / --deploy after vendored control: hosted Pipfile.lock + pristine pylock.toml
2026.0.0 yes pylock.toml only UPSTREAM PATCHED
2026.4.0 yes pylock.toml only UPSTREAM PATCHED
2026.8.0 yes pylock.toml only (2/2) UPSTREAM (sync and --deploy) PATCHED
≤ 2025.x n/a (no pylock support)
macOS / Windows not probed (the routing is pure path-presence logic)

First bad version

Not bisected. v4.0.0 has no pylock vendoring, so this is unreleased v5 behaviour on main b96a785. #1044 moved the tool-lock order into formats/governing_locks.rs (PYPI_TOOL_LOCKS), but the standalone-lock branch that wins here sits before that table and predates it.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi.rs:251 (doc, step 2) and :310–:324: if !has_uv_lock && matching_additional_lock { … return Ok((PypiFlavor::PythonLocks, warnings)) }. Any standalone pylock that contains the package beats Pipfile.lock, and nothing checks for a Pipfile or a [tool.pipenv] table in the pylock.
  • crates/socket-patch-core/src/formats/governing_locks.rs:115: PYPI_TOOL_LOCKS doesn't place Pipenv-generated pylocks relative to Pipfile.lock.

Related: #912 (pylock-only Pipenv checkout; Pipenv drops archive), #612 (pypi_multiple_lockfiles for a sibling requirements.txt), #1114 (inventory vs router disagreement on empty poetry / pdm locks).

Activity

  1. mikolalysenko commented on Oct 8, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Pipenv). Confirmed on main: crates/socket-patch-core/src/vendor/pypi.rs:310 returns PypiFlavor::PythonLocks for any standalone pylock that contains the package before the Pipfile.lock step is reached, with no check for a Pipfile or [tool.pipenv] generated_from. Not a duplicate of #912 (pylock-only checkout). Related to #1114 (router vs. lock inventory on which lock governs), but that one is about empty poetry/pdm locks and needs a different change, so they are not clustered. No open PR addresses it yet.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #912; shared root cause: the pylock readers/rewriters and the vendored flavor router treat pylock*.toml as installer-neutral and never check whether a Pipfile beside it makes Pipenv the installer). Branch: agent/fix-pipenv-pylock-consumer. Claim-ID: 2026-10-09T00:20:38Z-68f1ed


    Generated by Claude Code

  3. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1193


    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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions