[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
uv 0.6.15, 0.6.16 and 0.6.17 write lock revision = 2 with the artifact timestamp key spelled upload_time (underscore). uv 0.7.0 and later spell it upload-time, and uv 0.6.0–0.6.14 write no timestamp at all. The v5 upstream restore in patch/redirect/upstream/uv.rs accepts only upload-time as a sibling artifact field. On these locks every hosted unwind refuses with:
cannot restore pkg:pypi/six@1.16.0 to its upstream registry entry: uv.lock: sibling artifacts carry an unknown field `upload_time`; restore it from version control instead (`git checkout -- uv.lock`)
scan --mode hosted itself works on these locks (it wires the project, uv sync --locked installs the patched wheel, and vex attests). The patch can't be taken off again: rollback, remove and the hosted→vendored takeover all exit 1. Project locks and PEP 723 script locks are both affected. A pylock from uv pip compile on 0.6.17 uses the PEP 751 upload-time, and it rolls back byte-identically.
Impact
On a uv 0.6.15–0.6.17 project, a hosted patch can be applied but never unwound or migrated to vendored mode, apart from git checkout. A newer uv keeps the field as written until something re-locks, so a lock that 0.6.x wrote stays affected even after the user upgrades uv. docs/testing/uv-compatibility.md says lock revision 2 (0.6.15–0.8.3) is covered, and scripts/uv-vex-matrix.sh includes 0.6.17. CLI_CONTRACT's "Hosted unwind coverage" list of refusals (exclude-newer / no-binary / no-build, non-PyPI registries, [[distribution]]) doesn't include this case. This is a refusal that fires when it shouldn't.
Repro (Linux, real uv; mock patch API as in the bug-hunt harness)
pip install --target /tmp/uv0617 uv==0.6.17; UV="env PYTHONPATH=/tmp/uv0617 /tmp/uv0617/bin/uv"
mkdir app && cd app
printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "idna==3.7"]\n' > pyproject.toml
$UV lock && $UV sync
grep -c upload_time uv.lock # 4
cp pyproject.toml p.o; cp uv.lock l.o
socket-patch scan --mode hosted --yes --json ... # exit 0, six wired
socket-patch rollback --yes --json ... # exit 1: unknown field `upload_time`
socket-patch remove pkg:pypi/six@1.16.0 --yes ... # exit 1: same
socket-patch scan --mode vendored --yes ... # exit 1, redirect_revert_failed: same
The same steps with uv 0.7.22 restore byte-identically.
Expected vs actual
- Expected: the upstream restore re-derives the six entry in the sibling's shape, using whichever key spelling the sibling uses. CLI_CONTRACT says artifacts are re-derived "in the artifact shape a sibling registry package shows (which of
size / upload-time / … this uv release records)". The result is a byte-identical pyproject.toml / uv.lock, as on 0.7.x and later.
- Actual: refused, exit 1, with the files still pointing at the hosted wheel.
Matrix (main 045d7ec)
| uv |
lock key |
Linux |
macOS |
Windows |
| 0.6.14 and earlier |
(none) |
pass |
– |
– |
| 0.6.15 |
upload_time |
fail (rollback, twice) |
– |
– |
| 0.6.16 |
upload_time |
fail (rollback, twice) |
– |
– |
| 0.6.17 |
upload_time |
fail (rollback, remove, takeover, script lock) |
fail (all 4) |
fail (all 4) |
| 0.7.0 / 0.7.22 / 0.9.30 / 0.10.12 / 0.11.33 / 0.12.23 |
upload-time |
pass |
pass (0.7.22) |
pass (0.7.22) |
Probe run: https://github.com/SocketDev/socket-patch/actions/runs/37210059602
First bad
The unreleased v5 upstream restore. The known field check was added in de316b4 (#358). The released v4.0.0 predates the uv upstream restore.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:466: let known = ["url", "hash", "hashes", "size", "upload-time", "name"]; has no upload_time.
uv.rs:402 / uv.rs:419 / uv.rs:520 read and render only upload-time. The renderer also needs to write the key in the sibling's spelling.
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
uv 0.6.15, 0.6.16 and 0.6.17 write lock
revision = 2with the artifact timestamp key spelledupload_time(underscore). uv 0.7.0 and later spell itupload-time, and uv 0.6.0–0.6.14 write no timestamp at all. The v5 upstream restore inpatch/redirect/upstream/uv.rsaccepts onlyupload-timeas a sibling artifact field. On these locks every hosted unwind refuses with:scan --mode hosteditself works on these locks (it wires the project,uv sync --lockedinstalls the patched wheel, and vex attests). The patch can't be taken off again:rollback,removeand the hosted→vendored takeover all exit 1. Project locks and PEP 723 script locks are both affected. A pylock fromuv pip compileon 0.6.17 uses the PEP 751upload-time, and it rolls back byte-identically.Impact
On a uv 0.6.15–0.6.17 project, a hosted patch can be applied but never unwound or migrated to vendored mode, apart from
git checkout. A newer uv keeps the field as written until something re-locks, so a lock that 0.6.x wrote stays affected even after the user upgrades uv. docs/testing/uv-compatibility.md says lock revision 2 (0.6.15–0.8.3) is covered, andscripts/uv-vex-matrix.shincludes 0.6.17. CLI_CONTRACT's "Hosted unwind coverage" list of refusals (exclude-newer/no-binary/no-build, non-PyPI registries,[[distribution]]) doesn't include this case. This is a refusal that fires when it shouldn't.Repro (Linux, real uv; mock patch API as in the bug-hunt harness)
The same steps with uv 0.7.22 restore byte-identically.
Expected vs actual
size/upload-time/ … this uv release records)". The result is a byte-identical pyproject.toml / uv.lock, as on 0.7.x and later.Matrix (main
045d7ec)upload_timeupload_timeupload_timeupload-timeProbe run: https://github.com/SocketDev/socket-patch/actions/runs/37210059602
First bad
The unreleased v5 upstream restore. The
knownfield check was added in de316b4 (#358). The released v4.0.0 predates the uv upstream restore.Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/uv.rs:466:let known = ["url", "hash", "hashes", "size", "upload-time", "name"];has noupload_time.uv.rs:402/uv.rs:419/uv.rs:520read and render onlyupload-time. The renderer also needs to write the key in the sibling's spelling.