Skip to content

Hosted scan never sees the Pipfile, so a live Pipfile.lock conflict is treated as abandoned and a sibling requirements.txt is redirected anyway #333

Description

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

Summary

patch/redirect/pipenv.rs handles a conflicting Pipfile.lock entry (another version pinned, a foreign source, a VCS/path/file dependency) in one of two ways. It decides which by checking files.contains_key("Pipfile"):

  • Live lock (a Pipfile beside it): refuse the patch and veto the sibling pypi rewriters, "so nothing is half-redirected".
  • Abandoned lock (no Pipfile): refuse only this file, and still let requirements.txt / uv.lock / … be redirected.

But the disk hosted flow builds files from REDIRECT_CANDIDATE_FILES (crates/socket-patch-cli/src/commands/scan/hosted.rs:40), and that list contains Pipfile.lock but not Pipfile. So files.contains_key("Pipfile") is always false. Every live Pipenv project takes the "abandoned lock" branch, and:

  • the conflict veto never fires, so a sibling requirements.txt (for example one exported with pipenv requirements > requirements.txt for Docker or Heroku) is redirected;
  • the scan reports redirected: 1, status: success, exit 0;
  • the warning says (no Pipfile beside the lock: the sibling Python files are still redirected), which is false.

The unit tests in pipenv.rs pass a Pipfile key directly (pipenv.rs:558, :866), so they don't exercise the real file set.

Impact

Pipenv installs from Pipfile.lock, which still points at the user's own (unpatched) wheel. The scan still reports the patch as redirected. pipenv sync into a clean venv installs unpatched six (verified). vex does correctly refuse (no_applicable_patches), so there's no false attestation. But the scan's success and redirected count are wrong. The half-redirect is exactly the state that the comment at pipenv.rs:280-293 says the veto exists to prevent.

The same missing key also produces the misleading message in the vendored → hosted case reported on #328.

Repro (Linux; mock patch API as in the Poetry/uv bughunt probes, serving six 1.16.0)

mkdir c1 && cd c1 && mkdir wheels
cp six-1.16.0-py2.py3-none-any.whl wheels/          # the pristine PyPI wheel
cat > Pipfile <<'EOF'
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
six = {file = "wheels/six-1.16.0-py2.py3-none-any.whl"}
EOF
PIPENV_VENV_IN_PROJECT=1 pipenv install
echo 'six==1.16.0' > requirements.txt
git init -q
export SOCKET_API_URL=http://127.0.0.1:18080 SOCKET_API_TOKEN=fake SOCKET_ORG_SLUG=test-org
socket-patch scan --mode hosted --json --yes; echo "exit=$?"
# status: success, redirected: 1, rewrittenFiles: ["requirements.txt"]
# warning redirect_pipenv_refused: "Pipenv source for six already exists
#   (no Pipfile beside the lock: the sibling Python files are still redirected)"
cat requirements.txt
# six @ http://127.0.0.1:18080/patch/pypi/six/1.16.0/.../six-1.16.0-py2.py3-none-any.whl --hash=sha256:096e50…
rm -rf .venv && pipenv sync
pipenv run python -c "import six; print(hasattr(six, 'SOCKET_PATCHED'))"   # False

Reproduced twice on the current main (scan → rollback → scan).

Expected vs actual

  • Expected: per the rewriter's own contract (pipenv.rs:280-293), a conflict in a live Pipfile.lock means Pipenv won't pick the patch up, so the patch is refused project-wide. That means redirected: 0, requirements.txt untouched, and a redirect_pipenv_refused warning without the "no Pipfile" clause. docs/testing/pipenv-compatibility.md also describes the CLI as reading the project's Pipfile.
  • Actual: the patch is half-redirected into requirements.txt, the scan reports success with redirected: 1, and the warning claims there is no Pipfile.

OS × version

Pipenv (Linux) half-redirect of requirements.txt
2018.11.26 (py3.8) ❌ reproduces
2022.12.19 (py3.8) ❌ reproduces
2026.8.0 (py3.12) ❌ reproduces (twice)

The macOS and Windows cells are running in the probe below. The code path is pure file-set logic with no OS-specific branches.

First bad: 07a6b88 (#242, "Support hosted Pipenv patches and safe vendoring"), which introduced the guard. It first shipped in 4.0.0. Release 3.3.0 had no hosted mode.

Suspect code

  • crates/socket-patch-cli/src/commands/scan/hosted.rs:40: REDIRECT_CANDIDATE_FILES has no "Pipfile".
  • crates/socket-patch-core/src/patch/redirect/pipenv.rs:295: Err(PlanError::Conflict(detail)) if files.contains_key("Pipfile") is unreachable in the disk flow.
  • It may also be worth checking whether the in-memory engine's file set (crates/socket-patch-cli/src/hosted_memory/) carries Pipfile.

Probe run (macOS/Windows): https://github.com/SocketDev/socket-patch/actions/runs/36739663342

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