Skip to content

Vendored uv revert writes the pre-vendor specifier back into uv.lock after the user changes the vendored package's version spec, so uv sync --locked fails (vendor --revert / remove / rollback exit 0) #840

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

When vendored mode wires six through [tool.uv.sources], it rewrites six's entry in uv.lock's [package.metadata] requires-dist (or requires-dev) from { name = "six", specifier = "==1.16.0" } to { name = "six", path = ".socket/vendor/…" }. This matches uv, which drops the specifier for a path source (path_source_entry, crates/socket-patch-core/src/vendor/pypi_uv.rs:1450). It saves the old element as the record's original.

Because the specifier isn't in the lock any more, a later user edit to six's own requirement (uv add "six>=1.16,<1.17", or a hand edit plus uv lock) leaves uv.lock byte-identical. uv correctly sees nothing to relock, and uv sync --locked passes. The vendored record therefore still matches exactly, and vendor --revert / remove / rollback replace it with the saved original (pypi_uv.rs:830, through toml_surgery::replace_fragment, toml_surgery.rs:232). That writes back the stale specifier = "==1.16.0", while pyproject.toml now says six>=1.16,<1.17.

Result: both files lose every .socket/vendor reference and the command reports success (exit 0, no warning). But uv.lock's metadata no longer matches pyproject.toml, so uv sync --locked fails with "The lockfile at uv.lock needs to be updated".

Hosted mode does the right thing on the same shape. The vendored → hosted takeover (scan --mode hosted) and a hosted rollback after a single-clause edit both re-derive the current specifier from pyproject.toml, and --locked passes. Only the vendored revert path restores a recorded specifier blindly.

This is not #806 / #821. Those are about a relock that re-serializes an array, so the lock record drifts and only pyproject.toml is reverted. Here nothing in uv.lock drifts, both files are reverted, and the restored content itself is wrong. PR #822 (77686c4) fixes #806 / #821 but not this: verified, same failure.

Impact

  • Any project that tightens or relaxes the vendored package's requirement while the patch is vendored (a routine uv add pkg>=…) gets a broken frozen/locked install the moment the patch is reverted (vendor --revert, remove, rollback, and the GC pass behind scan --prune). CI on uv sync --locked goes red, while socket-patch reports success.
  • A plain uv sync silently relocks, which hides the corruption locally and makes it look like a lock churn caused by the user.

Repro

The mock serves POST /patch/package and the patched six 1.16.0 wheel with SRI sha512. It's the same harness as #806 / #821. uv resolves everything else from PyPI.

mkdir app && cd app
cat > pyproject.toml <<'EOF'
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["six==1.16.0", "attrs>=20"]
EOF
uv lock && uv sync
SP="socket-patch --api-url $MOCK --api-token fake --org acme --patch-server-url $MOCK --vendor-url $MOCK"
$SP scan --mode vendored --yes            # uv.lock requires-dist: { name = "six", path = ".socket/vendor/…" }
uv add "six>=1.16,<1.17"                  # pyproject changes; uv.lock is byte-identical (cmp says so)
uv sync --locked                          # ok
$SP vendor --revert --yes --json          # exit 0, status "success", no warning
grep 'name = "six", specifier' uv.lock    # { name = "six", specifier = "==1.16.0" }   <- stale
grep six pyproject.toml                   # "six>=1.16,<1.17"
uv sync --locked                          # error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.

The same happens with six in a PEP 735 dev group (requires-dev), with a hand edit plus uv lock instead of uv add, and with remove pkg:pypi/six@1.16.0 / rollback pkg:pypi/six@1.16.0 in place of vendor --revert. Other specifiers fail too (>=1.15, ==1.16.*).

Expected vs actual

  • Expected: the revert leaves a lock that matches the current pyproject.toml. Either it re-derives the requires-dist / requires-dev element from the current declaration, as hosted unwind already does (redirect/upstream/uv.rs), or, if it can't, it treats the mismatch as drift and keeps both files (docs/testing/uv-compatibility.md: "Script and lock edits are treated as a pair… rather than restoring only one side"; CLI_CONTRACT.md: user-changed fragments are left alone with vendor_lock_entry_drifted). A revert should never report success on a project whose uv sync --locked it just broke.
  • Actual: the stale original element is written back verbatim, exit 0, status: "success", no events, and uv sync --locked fails.

Matrix

On main 045d7ec, "fail" means 0 vendor refs in both files, uv.lock specifier ==1.16.0, and uv sync --locked exits non-zero. Each cell is {hand edit + uv lock, uv add} × {dependencies, dev group} × {vendor --revert, remove}. "H takeover" is scan --mode hosted on the same edited project.

OS uv 0.5.31 uv 0.8.17 uv 0.12.23 H takeover (all three)
Linux (sandbox + ubuntu-latest) fail (8/8) fail (8/8) fail (8/8) pass
macOS (macos-latest) fail (8/8) fail (8/8) fail (8/8) pass
Windows (windows-latest) fail (8/8) fail (8/8) fail (8/8) pass
Linux, PR #822 77686c4, vendor --revert (deps, dev) – – fail –

Not affected: a marker edit (six==1.16.0; python_version >= "3.9") on uv 0.8.17 / 0.12.23, because uv records nothing new and the restored ==1.16.0 still matches. On 0.5.31 the marker lands in the lock, which is the drift / half-revert case covered by #822.

Not bisected. The behaviour is inherent to the recorded-original design of the uv vendored backend and is present in every release on the matrix.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi_uv.rs:826-845: the uv_lock_requires_dist / uv_lock_requires_dev revert arm restores rec.original whenever rec.new is still present, with no check that the original's specifier still matches pyproject.toml's declaration.
  • crates/socket-patch-core/src/vendor/pypi_uv.rs:1450 (path_source_entry): the specifier is dropped (correctly, matching uv), which is why no lock drift can ever signal the user's edit.

Probe run: https://github.com/SocketDev/socket-patch/actions/runs/37284791550 (bughunt/uv/20261005-spec-edit-revert, 3 OS × 0.5.31 / 0.8.17 / 0.12.23).

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions