Skip to content

Vendored PDM < 2.11 gives no stale-install warning, so after pdm sync the unpatched release stays installed while vex attests not_affected #641

Description

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

Summary

PDM releases before 2.11 (lock_version 2 / 4.3 / 4.4) don't replace a package that's already installed at the same version. Hosted mode handles this: it emits redirect_pdm_stale_install_risk (plus the installed-byte probe redirect_pypi_stale_install), and vex then fails closed. Vendored mode handles neither. scan --mode vendored on a warm PDM env exits 0 with no stale warning. The next pdm sync / pdm install exits 0 but keeps the upstream bytes, and socket-patch vex attests not_affected.

The VEX envelope does carry a vendored_tree_out_of_sync warning, but its advice ("re-run your package manager's install to resync it") doesn't work on these PDM versions: pdm sync is exactly the step that keeps the old bytes.

Impact

A team on PDM 1.x / 2.0–2.10 vendors a patch into a project where the dependency is already installed (a dev machine, a cached CI venv) and runs pdm sync. Every command succeeds, yet the vulnerable code keeps running, and the published VEX says the vulnerability is mitigated. Hosted mode warns about this exact PDM behaviour, so vendored users get a weaker guarantee for the same lock.

Repro

Uses a local mock of the patch API that serves a patched urllib3 1.26.18 wheel, with a marker line appended to urllib3/response.py. Any free urllib3 1.26.18 patch shows the same thing.

uv venv tools/2.10.4 --python 3.11 && uv pip install --python tools/2.10.4/bin/python pdm==2.10.4
mkdir proj && cd proj
cat > pyproject.toml <<'EOF'
[project]
name = "x"
version = "0.0.0"
requires-python = ">=3.8"
dependencies = ["urllib3==1.26.18"]
[tool.pdm]
distribution = false
EOF
uv venv .venv --python ../tools/2.10.4/bin/python
export VIRTUAL_ENV=$PWD/.venv
../tools/2.10.4/bin/pdm lock && ../tools/2.10.4/bin/pdm sync      # warm env: upstream urllib3 installed
socket-patch scan --mode vendored --json --yes                     # exit 0; vendor events: only vendor_prebuilt_downloaded
../tools/2.10.4/bin/pdm sync                                       # exit 0, "All packages are synced to date"
grep -c SOCKET-PATCH-MARKER .venv/lib/python3.11/site-packages/urllib3/response.py   # 0 -> still upstream
socket-patch vex --json -O vex.json                                # exit 0; statement: not_affected / inline_mitigations_already_exist

For comparison, the same project with --mode hosted emits redirect_pdm_stale_install_risk and redirect_pypi_stale_install, and vex exits 1 (no_applicable_patches).

Expected vs actual

  • Expected: the same treatment as hosted on these locks. docs/testing/pdm-compatibility.md says PDM < 2.11 keeps an already-installed same-version package, and redirect/pdm.rs warns proactively once per lock for that reason. The Pipenv vendored path already probes the installed bytes (pypi_pipenv_stale_install, documented in docs/ecosystems.md). The vendored PDM path should warn with the remedy that works (pdm sync --reinstall, uninstall the package, or recreate the env), and the VEX hint shouldn't say a plain reinstall resyncs it.
  • Actual: no warning from the vendored scan. pdm sync / pdm install leave the env unpatched, and vex attests not_affected with only the generic vendored_tree_out_of_sync hint.

Matrix (Linux, main 045d7ec, real PDM)

PDM lock_version vendored scan warns? installed after pdm sync vex
1.4.5 2 no (only pypi_pdm_legacy_sync_required) upstream not_affected
1.5.3 2 no (only pypi_pdm_legacy_sync_required) upstream not_affected
2.8.2 4.3 no upstream not_affected
2.10.4 4.4 no upstream (pdm sync and pdm install; reproduced twice) not_affected
2.11.2 4.4.1 — patched (pass) not_affected (correct)
2.29.2 4.5.1 — patched (pass) not_affected (correct)
2.10.4, --mode hosted (control) 4.4 yes (redirect_pdm_stale_install_risk, redirect_pypi_stale_install) upstream fails closed

macOS and Windows weren't probed. The behaviour comes from PDM's installer, not the OS.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi_pdm.rs:95-97 only adds pypi_pdm_legacy_sync_required for lock_version == "2". There's no equivalent of the hosted redirect_pdm_stale_install_risk for 2 / 4.3 / 4.4.
  • crates/socket-patch-core/src/vendor/pypi.rs:606 (pipenv_stale_install_warning) is only called from the Pipenv flavor (pypi.rs:881), so the PDM flavor never probes the installed bytes.
  • crates/socket-patch-core/src/patch/redirect/pdm.rs:42-55 is the hosted counterpart to mirror.
  • crates/socket-patch-cli/src/commands/vex.rs:677-690 is the vendored_tree_out_of_sync advice.

Related but distinct: #477 (the reverse direction, after rollback).

Activity

  1. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (PDM). This is a correctness bug, because VEX attests not_affected while the installed bytes are still unpatched. The vendored PDM flavor never calls the installed-byte stale probe (pipenv_stale_install_warning is wired only into the Pipenv flavor), and it has no counterpart to hosted's redirect_pdm_stale_install_risk for lock_version 2, 4.3 or 4.4. I found no duplicate. It's related to #477 (the rollback direction) but has a separate cause.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] New information from the PDM bug-hunt routine (ledger #312), on main 045d7ec, Linux, real PDM, local mock patch API. Each cell was run twice.

    1. PEP 582 __pypackages__ hits this too. I used PDM 1.4.5 (legacy [tool.pdm]) and 1.5.3 ([project]) with use_venv = false and lock_version 2. I ran pdm sync (urllib3 1.26.18 lands in __pypackages__/3.8/lib), then scan --mode vendored --yes. The scan exits 0 with no stale-install warning; the only warning is PDM 0.x may regenerate freshly generated locks during install. After that, pdm sync exits 0 and leaves __pypackages__/3.8/lib/urllib3/_version.py unpatched. vex exits 0 and attests not_affected, adding only the generic vendored_tree_out_of_sync hint. So VEX does see the __pypackages__ tree; it just has no PDM-specific remedy.

    2. The remedy depends on the PDM version, and the hint's "re-run your package manager's install" doesn't work on any of these:

    PDM lock_version pdm sync after vendored scan pdm sync --reinstall
    1.4.5 2 unpatched no such flag (unrecognized arguments: --reinstall). Deleting __pypackages__/3.8/lib/urllib3* and running pdm sync installs the patched wheel
    1.5.3 2 unpatched no such flag
    2.8.2 4.3 unpatched patched, lock --check ok
    2.10.4 4.4 unpatched patched, lock --check ok
    1.8.5 / 1.12.8 / 2.0.3 3 / 3.1 / 4.0 refused (documented) —

    So a fix that adds a warning should probably say pdm sync --reinstall on PDM 2.x before 2.11. On PDM 1.x (lock_version 2) it should say to remove the installed package and re-sync, because --reinstall doesn't exist there.

    Minor: the "PDM 0.x may regenerate…" warning also fires on locks that PDM 1.4.5 and 1.5.3 generated. Both write lock_version 2, so the heuristic can't tell them apart. It's harmless.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Re-triage after the new data: priority:p1 stays. The scope now covers PEP 582 __pypackages__, and the remedy has to depend on the version: pdm sync --reinstall on PDM 2.x before 2.11, and remove-then-pdm sync on PDM 1.x / lock_version 2, which has no --reinstall. The root cause hasn't changed: the vendored PDM flavor has no stale-install probe. It's still unclaimed and eligible.


    Generated by Claude Code

  4. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    v5 triage: P2, not a release blocker. Retain the pre-PDM-2.11 stale-install warning at P2; current toolchains and the general reinstall guidance in #477 take precedence.

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

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