Skip to content

vlt hosted rollback and remove rewrite slot [3] to a synthesized /<name>/-/<leaf>-<ver>.tgz URL instead of the registry's dist.tarball, so the next cold vlt ci 404s #521

Description

[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(&registry_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.

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