[agent] Found by the scheduled PDM bug-hunt routine (ledger #312).
Summary
After a hosted pdm.lock rewrite, some PDM commands re-render the urllib3 block but keep our patch data:
pdm add <other> and pdm lock --update-reuse on PDM 2.26 and 2.29 keep the patch url and the patched hash.
- On PDM 2.12 – 2.20,
pdm add drops the url but keeps the patched files hash.
The first rollback then fails closed with a drift error that says to "re-run scan --mode hosted to normalize". After that re-scan, rollback reports success, lists reverted: ["pkg:pypi/urllib3@1.26.18"] and deletes the redirect ledger. But pdm.lock still carries the patch data:
- PDM 2.26 / 2.29: the lock keeps the hosted
url and the patched sha256. The project stays silently hosted-patched, with no ledger left, so no CLI path back to the registry lock remains.
- PDM 2.12 / 2.20: the lock keeps the patched sha256 with no
url, i.e. a registry source pinned to the patched wheel's hash. pdm sync then fails (InstallationError: Some package operations failed). Rollback leaves an uninstallable lock and reports success.
The re-scan treats the still-patched block as the pristine original and records it as the edit's original fragment. So the rebase is pristine → pristine instead of pristine → current.
Impact
Rollback silently lies. CI or a user who runs rollback to un-patch believes the project is back on upstream bytes. It is not, and on 2.12 – 2.20 it no longer installs at all. The doc's guarantee is broken: docs/testing/pdm-compatibility.md:64-67 says "A re-scan after a relock re-applies the patch and rebases the ledger's recorded edits onto the relocked text (pristine → current…), so rollback still lands on the pristine lock afterwards."
Repro (Linux, real PDM, mock patch API serving a real patched wheel)
printf '[project]\nname="proj"\nversion="0.1.0"\nrequires-python=">=3.8"\ndependencies=["urllib3==1.26.18"]\n[tool.pdm]\ndistribution=false\n' > pyproject.toml
pdm lock
socket-patch scan --mode hosted --json --yes $API # redirected 1
pdm add six==1.16.0 # keeps url= (2.29) / keeps patched hash only (2.20)
socket-patch rollback --json --yes $API # partial_failure: "content matches neither ... re-run `scan --mode hosted` to normalize"
socket-patch scan --mode hosted --json --yes $API # success, redirected 1
socket-patch rollback --json --yes $API # success, reverted [pkg:pypi/urllib3@1.26.18], editedFiles 1
grep -c '^url = ' pdm.lock # 1 on 2.29.2 (expected 0)
grep -c 1497f1666ebc pdm.lock # 1 = patched wheel sha256 still pinned (expected 0)
ls .socket/vendor/redirect-state.json # gone
pdm sync # 2.29.2: installs the PATCHED urllib3; 2.12.4/2.20.1: InstallationError
($API = --api-url http://127.0.0.1:<port> --api-token fake --org test against a local mock of /patches/batch, /patches/view, /patches/package. The mock serves an urllib3 1.26.18 wheel with a marker line appended to urllib3/response.py. No Socket token was used.)
Reproduced on 2 of 2 fresh runs on 2.29.2, and also without the intermediate failed rollback. A plain pdm lock, which re-resolves to the registry, is not affected: rollback lands on the relocked registry block there, as documented.
Expected vs actual
- Expected: after
scan → relock-style edit → scan → rollback, the urllib3 block has no Socket url and the upstream files hashes, per the doc quoted above. Otherwise rollback should fail closed rather than report success.
- Actual:
status: success, reverted: [purl], ledger removed, and the patched url/hash left in pdm.lock.
Matrix (Linux; the rewrite is platform-independent text handling)
| PDM |
post-rewrite command |
after re-scan + rollback |
pdm sync afterwards |
| 2.29.2 |
pdm add six==1.16.0 |
url + patched hash remain |
installs patched |
| 2.29.2 |
pdm lock --update-reuse |
url + patched hash remain |
installs patched |
| 2.29.2 |
pdm lock |
clean (registry) ✅ |
upstream |
| 2.26.9 |
pdm add six==1.16.0 |
url + patched hash remain |
installs patched |
| 2.20.1 |
pdm add six==1.16.0 |
patched hash, no url |
fails |
| 2.12.4 |
pdm add six==1.16.0 |
patched hash, no url |
fails |
macOS and Windows were not run for this one. The defect is in lock-text handling, which has no OS-specific code path.
Suspect code
crates/socket-patch-core/src/utils/pdm_lock.rs:383-399: plan_pdm_rewrite accepts an existing prior-hosted url (is_prior_hosted_url), then pdm_lock_fragments_in at :266 records that already-redirected block as the before/original fragment. A block whose files hash equals the patched artifact's sha256 (no url) is likewise taken as pristine.
- The rebase (pristine → current) for re-scans of hosted PDM edits should keep the ledger's earlier original when the current fragment still carries our URL or our artifact hash.
Tested on main f6b7fb9 (latest release v4.0.0).
[agent] Found by the scheduled PDM bug-hunt routine (ledger #312).
Summary
After a hosted
pdm.lockrewrite, some PDM commands re-render the urllib3 block but keep our patch data:pdm add <other>andpdm lock --update-reuseon PDM 2.26 and 2.29 keep the patchurland the patched hash.pdm adddrops theurlbut keeps the patchedfileshash.The first
rollbackthen fails closed with a drift error that says to "re-runscan --mode hostedto normalize". After that re-scan,rollbackreportssuccess, listsreverted: ["pkg:pypi/urllib3@1.26.18"]and deletes the redirect ledger. Butpdm.lockstill carries the patch data:urland the patched sha256. The project stays silently hosted-patched, with no ledger left, so no CLI path back to the registry lock remains.url, i.e. a registry source pinned to the patched wheel's hash.pdm syncthen fails (InstallationError: Some package operations failed). Rollback leaves an uninstallable lock and reports success.The re-scan treats the still-patched block as the pristine original and records it as the edit's
originalfragment. So the rebase is pristine → pristine instead of pristine → current.Impact
Rollback silently lies. CI or a user who runs
rollbackto un-patch believes the project is back on upstream bytes. It is not, and on 2.12 – 2.20 it no longer installs at all. The doc's guarantee is broken: docs/testing/pdm-compatibility.md:64-67 says "A re-scan after a relock re-applies the patch and rebases the ledger's recorded edits onto the relocked text (pristine → current…), sorollbackstill lands on the pristine lock afterwards."Repro (Linux, real PDM, mock patch API serving a real patched wheel)
(
$API=--api-url http://127.0.0.1:<port> --api-token fake --org testagainst a local mock of/patches/batch,/patches/view,/patches/package. The mock serves an urllib3 1.26.18 wheel with a marker line appended tourllib3/response.py. No Socket token was used.)Reproduced on 2 of 2 fresh runs on 2.29.2, and also without the intermediate failed rollback. A plain
pdm lock, which re-resolves to the registry, is not affected: rollback lands on the relocked registry block there, as documented.Expected vs actual
scan→ relock-style edit →scan→rollback, the urllib3 block has no Socketurland the upstreamfileshashes, per the doc quoted above. Otherwise rollback should fail closed rather than reportsuccess.status: success,reverted: [purl], ledger removed, and the patchedurl/hash left inpdm.lock.Matrix (Linux; the rewrite is platform-independent text handling)
pdm syncafterwardspdm add six==1.16.0pdm lock --update-reusepdm lockpdm add six==1.16.0pdm add six==1.16.0pdm add six==1.16.0macOS and Windows were not run for this one. The defect is in lock-text handling, which has no OS-specific code path.
Suspect code
crates/socket-patch-core/src/utils/pdm_lock.rs:383-399:plan_pdm_rewriteaccepts an existing prior-hostedurl(is_prior_hosted_url), thenpdm_lock_fragments_inat:266records that already-redirected block as thebefore/original fragment. A block whosefileshash equals the patched artifact's sha256 (no url) is likewise taken as pristine.Tested on main
f6b7fb9(latest release v4.0.0).