[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
When rollback / remove undo a hosted pin in vlt-lock.json, the v5 upstream restore takes slot [2] (integrity) from the registry's version document but builds slot [3] from the conventional npm path (<registry>/<name>/-/<leaf>-<version>.tgz). It ignores dist.tarball. vlt itself writes dist.tarball verbatim into slot [3]. So when a registry's tarball URL isn't the conventional one, rollback leaves a lock that differs from what vlt wrote and points at a URL the registry may not serve. With a cold cache, the next vlt ci then fails with a 404.
The npm restore (upstream/npm.rs:190) does use dist.tarball, and on the same mock registry it restores package-lock.json byte-for-byte. Only the vlt path synthesizes the URL.
Real registries publish non-conventional dist.tarball values. Examples: GitLab's and Artifactory's scoped …/@scope/name/-/@scope/name-1.0.0.tgz, GitHub Packages' …/download/@scope/name/1.0.0/<hash>, and tarballs served from a separate CDN path.
Impact
- After
socket-patch rollback (or remove <purl>) on a hosted vlt project, vlt ci with a cold cache fails Status: 404 for the package. Warm caches hide it, because vlt resolves by integrity.
- Even on a registry that also serves the conventional path, rollback isn't byte-exact: it leaves a spurious
vlt-lock.json diff, unlike npm.
Repro (local mock registry + patch server, real vlt)
The mock answers GET /left-pad with dist.tarball = http://127.0.0.1:18555/_cdn/files/left-pad-1.3.0.tgz. It serves only that path, not /left-pad/-/left-pad-1.3.0.tgz. SOCKET_PATCH_SERVER_URL / SOCKET_NPM_REGISTRY point at the mock.
echo '{"name":"app","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
echo '{"config":{"registries":{"npm":"http://127.0.0.1:18555/"}}}' > vlt.json
vlt install --allow-scripts :scripts && cp vlt-lock.json lock.orig
socket-patch scan --mode hosted --yes # slot [3] -> hosted URL; vlt ci -> patched (ok)
socket-patch rollback --yes # rc 0, "restored registry pins for 1 packages"
diff lock.orig vlt-lock.json
# < "~npm~left-pad@1.3.0": [0,"left-pad","sha512-ICFp…","http://127.0.0.1:18555/_cdn/files/left-pad-1.3.0.tgz"]
# > "~npm~left-pad@1.3.0": [0,"left-pad","sha512-ICFp…","http://127.0.0.1:18555/left-pad/-/left-pad-1.3.0.tgz"]
rm -rf node_modules ~/.cache/vlt ~/.local/share/vlt # cold cache
vlt ci --allow-scripts :scripts # rc 1, Status: 404 on /left-pad/-/left-pad-1.3.0.tgz
Control: lock.orig with the same cold cache passes vlt ci (left-pad resolves pristine).
A second shape is a scoped package with an Artifactory/GitLab-style tarball (/@sc/left-pad/-/@sc/left-pad-1.3.0.tgz). Rollback rewrites it to /@sc/left-pad/-/left-pad-1.3.0.tgz, and the cold vlt ci 404s the same way. socket-patch remove pkg:npm/left-pad@1.3.0 writes the same synthesized URL.
Expected vs actual
- Expected: rollback is the inverse of the hosted splice (module doc of
upstream/vlt.rs: "the inverse of the hosted node splice"). The restored slot [3] should be what vlt writes for the node, which is the registry's dist.tarball, still subject to vlt's "omit when under options.registry" rule. That's how the npm restore behaves.
- Actual: slot [3] =
npm_tarball_url(base, name, version), regardless of dist.tarball.
Matrix (Linux, main 61cfb9b)
| vlt |
non-conventional unscoped dist.tarball |
scoped /-/@scope/name-ver.tgz |
npm control (package-lock) |
| 1.0.10 |
repro (lock diff + cold vlt ci 404) |
— |
— |
| 1.2.0 |
repro |
— |
— |
| 1.3.3 |
repro (rollback and remove) |
repro |
byte-identical restore |
macOS / Windows are untested (probe branches are blocked this run). The defect is platform-independent string construction.
First bad: present since vlt hosted support landed (#269 / #280). Release 4.0.0 predates vlt support.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs:280: slot3 = records_url(...).then(|| tarball_url(®istry_base(...), name, version)). It should use the fetched dist.tarball (already in dists). The under_configured_registry check would then also compare that URL.
crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs:75: tarball_url → vendor::registry_fetch::npm_tarball_url.
- For comparison,
crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:190 uses dist.tarball.
[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
When
rollback/removeundo a hosted pin invlt-lock.json, the v5 upstream restore takes slot [2] (integrity) from the registry's version document but builds slot [3] from the conventional npm path (<registry>/<name>/-/<leaf>-<version>.tgz). It ignoresdist.tarball. vlt itself writesdist.tarballverbatim into slot [3]. So when a registry's tarball URL isn't the conventional one, rollback leaves a lock that differs from what vlt wrote and points at a URL the registry may not serve. With a cold cache, the nextvlt cithen fails with a 404.The npm restore (
upstream/npm.rs:190) does usedist.tarball, and on the same mock registry it restorespackage-lock.jsonbyte-for-byte. Only the vlt path synthesizes the URL.Real registries publish non-conventional
dist.tarballvalues. Examples: GitLab's and Artifactory's scoped…/@scope/name/-/@scope/name-1.0.0.tgz, GitHub Packages'…/download/@scope/name/1.0.0/<hash>, and tarballs served from a separate CDN path.Impact
socket-patch rollback(orremove <purl>) on a hosted vlt project,vlt ciwith a cold cache failsStatus: 404for the package. Warm caches hide it, because vlt resolves by integrity.vlt-lock.jsondiff, unlike npm.Repro (local mock registry + patch server, real vlt)
The mock answers
GET /left-padwithdist.tarball = http://127.0.0.1:18555/_cdn/files/left-pad-1.3.0.tgz. It serves only that path, not/left-pad/-/left-pad-1.3.0.tgz.SOCKET_PATCH_SERVER_URL/SOCKET_NPM_REGISTRYpoint at the mock.Control:
lock.origwith the same cold cache passesvlt ci(left-pad resolvespristine).A second shape is a scoped package with an Artifactory/GitLab-style tarball (
/@sc/left-pad/-/@sc/left-pad-1.3.0.tgz). Rollback rewrites it to/@sc/left-pad/-/left-pad-1.3.0.tgz, and the coldvlt ci404s the same way.socket-patch remove pkg:npm/left-pad@1.3.0writes the same synthesized URL.Expected vs actual
upstream/vlt.rs: "the inverse of the hosted node splice"). The restored slot [3] should be what vlt writes for the node, which is the registry'sdist.tarball, still subject to vlt's "omit when underoptions.registry" rule. That's how the npm restore behaves.npm_tarball_url(base, name, version), regardless ofdist.tarball.Matrix (Linux, main
61cfb9b)dist.tarball/-/@scope/name-ver.tgzvlt ci404)macOS / Windows are untested (probe branches are blocked this run). The defect is platform-independent string construction.
First bad: present since vlt hosted support landed (#269 / #280). Release 4.0.0 predates vlt support.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs:280:slot3 = records_url(...).then(|| tarball_url(®istry_base(...), name, version)). It should use the fetcheddist.tarball(already indists). Theunder_configured_registrycheck would then also compare that URL.crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs:75:tarball_url→vendor::registry_fetch::npm_tarball_url.crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:190usesdist.tarball.