You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
vlt hosted rollback, remove and the hosted→vendored takeover restore a vlt 1.3 brotli node as a gzip node (flag bit 4, the .tar.br integrity and URL are lost) #941
[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
#820 (the fix for #372) made socket-patch accept vlt 1.3 lock nodes that carry the brotli flag (slot [0] bit 4). A hosted pin now correctly clears bit 4 when it points the node at the patched .tgz. But the hosted upstream restore used by rollback, remove and the hosted→vendored takeover (crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs) never puts the brotli form back. It rebuilds slot [2] from the registry's dist.integrity and slot [3] from the conventional .tgz URL, then derives bit 4 from that .tgz URL (brotli_for_slot3(slot3) at upstream/vlt.rs:272), so the bit is always cleared. Nothing in the restore path reads the packument's dist.alternates (grep -rn alternates crates/ finds nothing).
So after a rollback, the node vlt originally wrote as
A dev node goes from 6 to 2 the same way. The #372 triage comment had already flagged that rollback/remove "will need to put bit 4 back if the hosted rewrite clears it", and the #820 commit message says "reverts put the recorded bit back with the original slots". That holds for the vendored revert (byte-exact in my runs) and for carried_pin_original, but not for the hosted upstream restore.
Impact
rollback / remove don't undo the scan: vlt-lock.json differs from the committed, pre-scan lock in slots [0], [2] and [3], so users see an unexplained lock diff after a "successful" rollback.
vlt doesn't heal it. vlt install and vlt ci keep the gzip node indefinitely (checked on 1.3.6 and 1.3.7), so the project silently loses vlt 1.3's Brotli tarball selection for that package until someone deletes the lock or runs vlt update.
The install itself still works: the restored node is self-consistent, and vlt ci installs pristine bytes. So this isn't a security or availability issue. It's a broken rollback contract for vlt ≥ 1.3.1 projects on registries that advertise tar.br alternates (e.g. vlt's own registry).
Repro (Linux, Node 22.22.0, main 9c43dfc)
The mock is the run-2 probe mock from ledger #307: mock.mjs serves a registry on :18555 plus the patch API on :18556. The left-pad@1.3.0 packument advertises dist.alternates: [{kind:"tar.br", …}], and the mock serves a free hosted patch. vlt is run with --allow-scripts :scripts only because the sandbox blocks vlt's security-data fetch.
The same happens with socket-patch remove pkg:npm/left-pad@1.3.0, and with scan --mode vendored over the hosted pin followed by rollback (the takeover's upstream restore writes the gzip node, and the vendored ledger then records that gzip node as the original). For a brotli dev node, flag 6 comes back as 2.
For comparison, a straight scan --mode vendored followed by rollback on the same brotli lock is byte-exact.
Expected vs actual
Expected: docs/ecosystems.md ("Hosted vlt pins need no ledger (v5.0): rollback restores them from the npm registry") and CLI_CONTRACT.md's vlt restore say the node gets the registry's entry back. For a registry that advertises a tar.br alternate, the entry vlt 1.3 writes is the brotli one: bit 4 set, the alternate's integrity in slot [2] and its URL in slot [3]. That's what the pre-scan lock held, and what a fresh vlt install writes. Fix vlt 1.3 brotli lock nodes being refused (#372) #820 states that reverts put the recorded bit back.
Actual: the restore always writes the gzip dist.integrity, the .tgz URL and bit 4 cleared.
Matrix
OS
vlt
hosted rollback
hosted remove
hosted→vendored takeover + rollback
vendored-only rollback
Linux
1.3.6
fail
fail
not run
pass (byte-exact)
Linux
1.3.7
fail (×2)
fail
fail
pass (byte-exact)
Linux
1.2.0 / any vlt, registry without tar.br
pass
pass
pass
pass
macOS and Windows weren't run (probe branches are blocked for this routine). The code path is OS-independent.
First bad
Not a regression. Before #820 (7fd88f5), brotli locks were refused outright (#372). Every commit since then restores the gzip form.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs:236-275: fetch_dists returns only dist.integrity, slot [3] is built from tarball_url(...) (:266), and bit 4 is brotli_for_slot3(slot3) (:272).
[agent] Found by the scheduled vlt bug-hunt routine (ledger #307).
Summary
#820 (the fix for #372) made socket-patch accept vlt 1.3 lock nodes that carry the brotli flag (slot [0] bit 4). A hosted pin now correctly clears bit 4 when it points the node at the patched
.tgz. But the hosted upstream restore used byrollback,removeand the hosted→vendored takeover (crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs) never puts the brotli form back. It rebuilds slot [2] from the registry'sdist.integrityand slot [3] from the conventional.tgzURL, then derives bit 4 from that.tgzURL (brotli_for_slot3(slot3)atupstream/vlt.rs:272), so the bit is always cleared. Nothing in the restore path reads the packument'sdist.alternates(grep -rn alternates crates/finds nothing).So after a rollback, the node vlt originally wrote as
comes back as
A dev node goes from
6to2the same way. The #372 triage comment had already flagged that rollback/remove "will need to put bit 4 back if the hosted rewrite clears it", and the #820 commit message says "reverts put the recorded bit back with the original slots". That holds for the vendored revert (byte-exact in my runs) and forcarried_pin_original, but not for the hosted upstream restore.Impact
rollback/removedon't undo the scan:vlt-lock.jsondiffers from the committed, pre-scan lock in slots [0], [2] and [3], so users see an unexplained lock diff after a "successful" rollback.vlt installandvlt cikeep the gzip node indefinitely (checked on 1.3.6 and 1.3.7), so the project silently loses vlt 1.3's Brotli tarball selection for that package until someone deletes the lock or runsvlt update.vlt ciinstalls pristine bytes. So this isn't a security or availability issue. It's a broken rollback contract for vlt ≥ 1.3.1 projects on registries that advertisetar.bralternates (e.g. vlt's own registry).Repro (Linux, Node 22.22.0, main
9c43dfc)The mock is the run-2 probe mock from ledger #307:
mock.mjsserves a registry on :18555 plus the patch API on :18556. Theleft-pad@1.3.0packument advertisesdist.alternates: [{kind:"tar.br", …}], and the mock serves a free hosted patch. vlt is run with--allow-scripts :scriptsonly because the sandbox blocks vlt's security-data fetch.The same happens with
socket-patch remove pkg:npm/left-pad@1.3.0, and withscan --mode vendoredover the hosted pin followed byrollback(the takeover's upstream restore writes the gzip node, and the vendored ledger then records that gzip node as the original). For a brotli dev node, flag6comes back as2.For comparison, a straight
scan --mode vendoredfollowed byrollbackon the same brotli lock is byte-exact.Expected vs actual
rollbackrestores them from the npm registry") and CLI_CONTRACT.md's vlt restore say the node gets the registry's entry back. For a registry that advertises atar.bralternate, the entry vlt 1.3 writes is the brotli one: bit 4 set, the alternate's integrity in slot [2] and its URL in slot [3]. That's what the pre-scan lock held, and what a freshvlt installwrites. Fix vlt 1.3 brotli lock nodes being refused (#372) #820 states that reverts put the recorded bit back.dist.integrity, the.tgzURL and bit 4 cleared.Matrix
tar.brmacOS and Windows weren't run (probe branches are blocked for this routine). The code path is OS-independent.
First bad
Not a regression. Before #820 (
7fd88f5), brotli locks were refused outright (#372). Every commit since then restores the gzip form.Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/vlt.rs:236-275:fetch_distsreturns onlydist.integrity, slot [3] is built fromtarball_url(...)(:266), and bit 4 isbrotli_for_slot3(slot3)(:272).crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:28(fetch_dists) doesn't surfacedist.alternates./<name>/-/<leaf>-<ver>.tgzURL instead of the registry's dist.tarball, so the next coldvlt ci404s #521 / PR Fix npm-family restore ignoring project registry (#908, #521) #918 switch slot [3] todist.tarball, but they also ignore thetar.bralternate, so they don't fix this.