Skip to content

Agent mode writes the patch into PDM's shared install cache when PDM 2.0–2.12 installs packages as directory symlinks (install.cache + symlink) #332

Description

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

Summary

On PDM 2.0 – 2.12 with install.cache = true, the symlink cache method (the default method) installs each top-level package as a directory symlink into PDM's global cache: site-packages/urllib3 -> ~/.cache/pdm/packages/urllib3-1.26.18-py2.py3-none-any/lib/urllib3. Agent mode (scan --mode agent / apply) stages and renames the patched file inside that directory. The directory is the shared cache directory, so the patch lands in PDM's cache and not in the project.

The rename-over write in apply.rs protects a file symlink or hardlink. It does not protect a symlinked parent directory.

Impact

  • Every other project on the machine that shares the PDM cache silently gets the patched bytes, on its next pdm sync and in any environment already linked. Those projects have no .socket/ state, no manifest record and no VEX trail.
  • rollback in one project un-patches all the others, because they share the same inode tree.
  • PDM's cache entry no longer matches the wheel it was unpacked from. PDM does not re-verify the cache, so the corrupted entry persists until pdm cache clear.
  • This contradicts the stated invariant in crates/socket-patch-core/src/patch/apply.rs:412-418: "a symlink into a store is replaced by a private regular file instead of being written through".

PDM 2.15+ (and 2.x hardlink) link individual files, so the rename breaks the link and the cache stays intact there. The pth method (2.10.4) keeps the files in the cache only; agent apply fails closed with File not found, which is fine.

Repro (Linux, real PDM, mock patch API serving urllib3/response.py = original + marker line)

uv venv -p 3.11 pdmenv && VIRTUAL_ENV=$PWD/pdmenv uv pip install pdm==2.12.4
export HOME=$PWD/home PDM_CHECK_UPDATE=false
PDM=$PWD/pdmenv/bin/pdm
$PDM config install.cache true; $PDM config install.cache_method symlink; $PDM config venv.in_project true
for p in a b; do mkdir $p; printf '[project]\nname="proj-%s"\nversion="0.1.0"\nrequires-python=">=3.8"\ndependencies=["urllib3==1.26.18"]\n[tool.pdm]\ndistribution=false\n' $p > $p/pyproject.toml; done
(cd a && $PDM lock && $PDM sync)
ls -la a/.venv/lib/python3.11/site-packages/ | grep urllib3      # urllib3 -> $HOME/.cache/pdm/packages/.../lib/urllib3
(cd a && socket-patch scan --mode agent --json --yes --api-url http://127.0.0.1:18183 --api-token fake --org test)
grep -c SOCKET-MOCK-MARKER $HOME/.cache/pdm/packages/urllib3-1.26.18-py2.py3-none-any/lib/urllib3/response.py   # 1  <- cache patched
(cd b && cp ../a/pdm.lock . && $PDM sync)
grep -c SOCKET-MOCK-MARKER b/.venv/lib/python3.11/site-packages/urllib3/response.py   # 1  <- never-scanned project patched
ls b/.socket                                                                           # does not exist

(The mock serves /patches/batch, /patches/view/<uuid> and /patches/blob/<hash>. No Socket token was used.) A self-contained script (probe.py: a mock API in a thread, two projects and the checks) is embedded in the probe workflow below. Locally it reproduced on 5 of 5 fresh runs. The probe reproduced it on ubuntu-latest, macos-latest and windows-latest.

Expected vs actual

  • Expected: agent mode patches only the project's own environment. The docs say shared stores are never written through (pnpm, bun, uv and the Go module cache are all covered by the copy-on-write rename, and vlt's shared store is called out in README). A symlinked package directory should be materialised as a private copy first, or refused with an error.
  • Actual: the write follows the directory symlink into ~/.cache/pdm/packages/…. scan exits 0 with applied: 1.

OS × PDM matrix (install.cache=true)

PDM cache_method Linux macOS Windows
2.0.3 symlink (dir) writes through (local) — —
2.8.2 symlink (dir) writes through (local + CI) writes through writes through
2.10.4 symlink (dir) writes through (local) — —
2.11.2 symlink (dir) writes through (local) — —
2.12.4 symlink (dir) writes through (local + CI) writes through writes through
2.15.4 / 2.20.1 / 2.26.9 / 2.29.2 symlink (per-file) OK OK (2.15.4, 2.29.2) OK
2.12.4 – 2.29.2 hardlink OK OK OK
2.10.4 pth fails closed (File not found), OK — —

PDM 1.x (feature.install_cache) was not tested.

Suspect code

  • crates/socket-patch-core/src/patch/apply.rs:496-513: the stage-and-rename is done in filepath.parent(), which resolves through the symlinked site-packages/<pkg> directory.
  • crates/socket-patch-core/src/utils/fs.rs:457 (atomic_write_bytes)

Neither checks whether any ancestor between the package root (site-packages) and the file is a symlink.

Probe run: https://github.com/SocketDev/socket-patch/actions/runs/36735829162 (tested on main f6b7fb9, latest release v4.0.0).

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