Skip to content

After a hosted or vendored PDM rollback, pdm sync / pdm install keep the patched build installed, though rollback says the next install restores it #477

Description

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

Summary

socket-patch rollback on a hosted or vendored PDM project restores pdm.lock byte for byte, and then prints:

Note: 1 unwired package keeps its patched bytes in installed trees until the next package-manager install.

(JSON: warnings[] reinstall_required, with the same text.)

On PDM that isn't true. The installed copy is the same version as the restored lock entry, so PDM treats it as up to date: pdm sync, pdm install and pdm install --frozen-lockfile all report All complete! 0/0 and keep the patched build. Its direct_url.json still points at the patch-server URL, or at the now-deleted .socket/vendor/pypi/<uuid>/…whl. Only pdm sync --reinstall, or a fresh venv, brings back the upstream bytes.

PDM's synchronizer only reinstalls a same-version package when the locked candidate is a URL/file that differs from the installed one. That's why the forward direction (registry → hosted/vendored) installs the patch on 2.12+/2.29, and the reverse direction (URL/file → registry) never does.

Impact

  • Rollback is the escape hatch when a patch breaks something. On PDM, every existing environment (developer venvs, cached CI venvs, long-lived servers) keeps running the patched code after the documented "rollback, then install" flow. The CLI's own note tells the user that flow is enough.
  • Afterwards, nothing in socket-patch can see the leftover. .socket/ is gone, so vex and list have nothing to report, and a report-only scan just offers the patch again.
  • The CLI already knows PDM keeps same-version installs in the forward direction (redirect_pdm_stale_install_risk for PDM < 2.11, and the Python stale-install guard in CLI_CONTRACT "Python stale-install guard"). The rollback direction has no equivalent, and this reverse case affects every PDM version tested, including the latest.

Repro (Linux, real PDM, local mock of the patch API serving a urllib3 1.26.18 wheel whose urllib3/response.py carries a marker line)

# usage: repro.sh <pdm-binary> <hosted|vendored> <workdir>
PDM=$1; MODE=$2; W=$3
mkdir -p "$W/p"; cd "$W/p"
C="--api-url http://127.0.0.1:18183 --api-token fake --org test -e pypi"
printf '[project]\nname="p"\nversion="0.1.0"\nrequires-python=">=3.8"\ndependencies=["urllib3==1.26.18"]\n[tool.pdm]\ndistribution=false\n' > pyproject.toml
$PDM config venv.in_project true; $PDM lock; $PDM sync; cp pdm.lock pdm.lock.orig
F=$(ls .venv/lib/python3*/site-packages/urllib3/response.py)
socket-patch scan --mode "$MODE" --yes $C; $PDM sync          # marker present (patched)  OK
socket-patch rollback $C                                      # "Note: ... until the next package-manager install."
cmp pdm.lock pdm.lock.orig                                    # identical                 OK
$PDM sync                                                     # "All complete! 0/0"
grep -c SOCKET-MOCK-MARKER "$F"                               # 1   <- still patched
$PDM install --frozen-lockfile; grep -c SOCKET-MOCK-MARKER "$F"   # 1   <- still patched
cat "$(dirname "$F")/../urllib3-1.26.18.dist-info/direct_url.json"  # url = patch server / deleted .socket/vendor wheel
$PDM sync --reinstall; grep -c SOCKET-MOCK-MARKER "$F"        # 0   (only this restores upstream)

(Env used: SOCKET_PATCH_SERVER_URL=<mock>, and for the hosted restore SOCKET_PYPI_JSON_API=<local pass-through to pypi.org>, because the sandbox blocks the Socket hosts. No Socket token was used.)

Expected vs actual

  • Expected: after rollback, the "next package-manager install" the note promises gives back the upstream bytes. Failing that, rollback should name the command that does, the way the forward-direction guards do (CLI_CONTRACT "Python stale-install guard": "Reinstall from the rewritten lock in the affected interpreter…"), e.g. pdm sync --reinstall (or pdm sync --reinstall <pkg>). docs/testing/pdm-compatibility.md only says "rollback … restores the pristine lock", and the backtest asserts only the lock bytes.
  • Actual: the lock is pristine, but PDM's normal install commands are no-ops. The patched build stays installed indefinitely, and the CLI's note says otherwise.

OS × version (Linux, main 6e7ef74)

PDM lock_version vendored rollback → pdm sync / install --frozen-lockfile hosted rollback → same pdm sync --reinstall
2.29.2 4.5.x still patched (2/2 runs) still patched (2/2) upstream restored
2.12.4 4.4.1 still patched (2/2) still patched (1/1 + manual) upstream restored

macOS and Windows weren't probed (no probe branch this run). PDM's same-version rule lives in its synchronizer, not in anything OS-specific, so I expect the same result there.

Suspect code

  • crates/socket-patch-cli/src/commands/rollback.rs:1677-1686: the generic reinstall_required note ("until the next package-manager install") is emitted for every ecosystem. PDM (and possibly other Python installers that skip same-version installs) needs the explicit reinstall command, or a probe of the installed copy like the forward-direction stale-install guard does.

Backlog review — 2026-10-08

Priority: P1 → P2. PDM reinstall advice after rollback is wrong; a forced clean reinstall is available. Keep as a functional rollback UX issue.

Activity

  1. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] One more cell: PDM 1.4.5 (lock_version 2, legacy [metadata.files], PEP 582 __pypackages__) behaves the same way. Hosted scan, then pdm sync installs the patched bytes, then rollback restores the lock byte for byte (hosted.reverted: [pkg:pypi/urllib3@1.26.18]). After that, pdm sync prints 🎉 All complete! and __pypackages__/3.8/lib/urllib3/response.py is still patched. So this covers PDM 1.4.5, 2.12.4 and 2.29.2.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (PDM). No duplicate or open fix PR found.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] New information from the scheduled uv bug-hunt routine (ledger #310): uv has the same defect, with the same root cause (the generic reinstall_required note in crates/socket-patch-cli/src/commands/rollback.rs, around lines 1714–1723 on 61cfb9b). Adding it here instead of filing a duplicate.

    After a hosted rollback or a vendored rollback / vendor --revert on a uv project, the lock files are restored byte for byte. But uv treats the same-version URL/file install as satisfying the registry pin, so the patched wheel stays installed (direct_url.json still points at the patch server, or at the deleted .socket/vendor/pypi/<uuid>/…whl).

    uv project (uv.lock), Linux, main 61cfb9b, six 1.16.0 from a local mock patch API. Each row reproduced at least twice:

    uv hosted rollback → uv sync --locked / --frozen / plain uv sync vendored revert → same uv sync --reinstall-package six
    0.2.37, 0.4.30, 0.5.31, 0.8.17 still patched ("Audited 2 packages") still patched (0.8.17) upstream restored
    0.8.18, 0.8.19, 0.8.20, 0.8.22, 0.8.24, 0.9.0, 0.10.0, 0.11.0, 0.12.0, 0.12.21 upstream reinstalled (+ six==1.16.0) upstream reinstalled (0.12.21) –

    So for uv.lock projects, uv fixed this on its side in 0.8.18. Every uv from 0.2.35 (native locks) through 0.8.17 is affected.

    uv requirements lane (uv pip sync / uv pip install -r requirements.txt): after the unwind restores six==1.16.0 with its hashes, the patched wheel stays installed on every OS and version probed. That covers ubuntu, macos and windows × uv 0.1.44 / 0.5.31 / 0.12.21, hosted and vendored, LF and CRLF (12/12 cells per OS, 36 in total; probe run https://github.com/SocketDev/socket-patch/actions/runs/36923586580). uv reports "Audited/Checked 2 packages". Only uv pip install --reinstall six or a fresh venv restores upstream. In those same cells the forward direction (fresh install is patched) and the byte-identical unwind both pass.

    Two more details:

    • socket-patch vendor --revert (the vendored-only unwind) prints no advisory at all: JSON events: [{action: removed}], no warnings. rollback at least prints the (inaccurate) note.
    • After the unwind, vex exits 2 (manifest_not_found), so nothing in socket-patch can see the leftover patched install.

    Repro (uv 0.8.17):

    printf '[project]\nname="app"\nversion="1.0.0"\nrequires-python=">=3.8"\ndependencies=["six==1.16.0","idna==3.6"]\n' > pyproject.toml
    uv lock && uv sync
    socket-patch scan --mode hosted --yes <api flags>; uv sync --frozen     # six patched
    socket-patch rollback --yes <api flags>                                 # "Note: … until the next package-manager install."
    git diff --exit-code uv.lock pyproject.toml                             # pristine
    uv sync --locked; uv sync --frozen; uv sync                             # "Audited 2 packages"
    python -c 'import six; print(getattr(six,"SOCKET_PATCHED",0))'          # 1  <- still patched
    uv sync --reinstall-package six                                         # only this restores upstream

    A fix for the uv case would name uv sync --reinstall-package <pkg> (uv.lock / script locks) and uv pip install --reinstall <pkg> (requirements / pylock), the same way the forward-direction redirect_pypi_stale_install names a remedy.


    Generated by Claude Code

  4. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] One more PDM entry point: remove <purl> hits the same leftover-install path, and it prints no advisory at all. Rollback at least prints the (inaccurate) "until the next package-manager install" note.

    PDM 2.29.2, main 61cfb9b, vendored urllib3 1.26.18 (2/2 runs): scan --mode vendored → pdm sync (patched) → socket-patch remove pkg:pypi/urllib3@1.26.18 --yes exits 0 with only "Reverted vendoring for …". pdm.lock is restored byte for byte, and .socket/ is gone. Then pdm sync prints All complete! 0/0, and direct_url.json still points at the deleted .socket/vendor/pypi/<uuid>/…whl with the patched bytes installed. A fix for #477 should cover remove (and vendor --revert, per the uv comment above), not only rollback.


    Generated by Claude Code

  5. mikolalysenko commented on Oct 7, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] New information from the scheduled Pipenv bug-hunt routine (ledger #313): Pipenv has the same defect. The cause is the same: the generic reinstall_required note in crates/socket-patch-cli/src/commands/rollback.rs (~1636). I'm adding it here instead of filing a duplicate.

    Pipenv never reinstalls a release that is already installed. pipenv-compatibility.md documents this for the forward direction ("Warm virtualenvs are never reinstalled"), and the scan's redirect_pypi_stale_install / pypi_pipenv_stale_install warnings name a Pipenv remedy for it. Rollback has no equivalent, so after an unwind the patched wheel stays installed for good.

    Linux, main 9c43dfc, six 1.16.0 from a local mock patch API. Steps: scan --mode hosted|vendored → pipenv sync (PATCHED) → socket-patch rollback --yes. Pipfile.lock is restored byte for byte, and rollback prints Note: 1 unwired package keeps its patched bytes in installed trees until the next package-manager install. Then pipenv sync and pipenv install --deploy both exit 0, and six is still patched. Each cell ran 2/2:

    Pipenv hosted rollback → sync / --deploy vendored rollback → sync / --deploy
    2022.12.19 still patched still patched
    2023.12.1 still patched still patched
    2026.8.0 still patched still patched
    • remove pkg:pypi/six@1.16.0 (hosted and vendored) and vendor --revert give the same result, and they print no advisory at all.
    • The remedy that works is the one scan already prints: pipenv run pip uninstall -y six && pipenv sync. That restores upstream on 2026.8.0, and so does pipenv --rm && pipenv sync.
    • Agent → vendored variant: scan --mode agent patches the venv in place, then scan --mode vendored moves the manifest record into the vendor ledger ("1 manifest record moved to the vendor ledger"). After that, rollback reverts the lock and GCs .socket/blobs, but the venv's in-place agent edit stays (PATCHED, on 2022.12.19 and 2026.8.0). Pipenv won't overwrite it, and socket-patch has no record or before-blob left to undo it. The agent → hosted variant, by contrast, keeps the manifest record, so rollback restores the venv to upstream.

    A fix for Pipenv would print the same remedy as the forward warning: pipenv run pip uninstall -y <pkg> && pipenv sync with the lock's --dev / --categories arguments, or pipenv --rm && pipenv sync.


    Generated by Claude Code

  6. added
    v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.
    uxCLI commands, help, diagnostics, output consistency, or actionable recovery instructions.
    and removed on Oct 8, 2026
  7. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 release blocker (P1). The normal rollback reinstall instruction is ineffective for PDM/Pipenv. Print the actual reinstall command needed to restore the upstream bytes.

    This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.

  8. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming for v5 blocker burn-down (shared root cause: generic reinstall_required rollback note ignores that PDM/Pipenv/uv keep same-version URL/file installs). Branch: agent/v5-pypi-rollback-reinstall. Claim-ID: 2026-10-09T16:41:34Z-dd0131

  9. added 2 commits that reference this issue on Oct 9, 2026
    1c9d03b
    ae6025b
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

    agent:claimedagent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:pdmPDMpriority:p1uxCLI commands, help, diagnostics, output consistency, or actionable recovery instructions.v5-blockerMust resolve before v5: public interface/migration or ordinary patch-install-undo failure.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions