Skip to content

Vendored uv repair pairs a hashless pure wheel with another wheel's hash, because ledger recovery re-parses uv.lock with its own scanner #1079

Description

[agent] Filed by the scheduled architecture audit routine (ecosystems and formats). Register: E89.

Kind: bug (with a duplicated predicate). Source: new finding; register E89.

Problem

Verified on main 05ecc6e. Two parts of vendor/lock_inventory pick "the hash-pinned pure wheel" of a uv.lock [[package]], and they parse it differently.

  • Live inventory: python_package_archive reads the lock with toml_edit through the shared utils::python_lock::package_artifacts. It pairs each artifact's url with that artifact's hash.
  • Ledger recovery (repair / re-vendor of a missing artifact): pure_wheel_from_uv_unit, called at recover.rs#L239, is a string scanner. It finds url = ", then takes the first hash = "sha256: anywhere after it (rest.find(...)), even when that hash belongs to a later wheel. Its doc and its test claim the opposite ("fail-closed, never a guessed pairing").

The "is this a pure wheel" rule is also written four times:

Site Rule
pypi_distribution::wheel_platform_from_filename Tag triple parsed. Shared by vendored (vendor_platform_locked) and hosted (redirect_pypi_platform_wheel, redirect/mod.rs#L580).
lock_inventory/pypi.rs#L313 url.split(['?','#'])…ends_with("-none-any.whl")
lock_inventory/pypi.rs#L393 (poetry) file.ends_with("-none-any.whl")
recover.rs#L464 url.ends_with("-none-any.whl"), with no ?/# strip

On main the suffix rule and the shared classifier agree on well-formed names. Open PR #1053 (fix for #1048) changes only the shared classifier: cp311-none-any, pp310-none-any and py2-none-any become non-portable. The three inventory copies will keep choosing those wheels as "pure", so the modes drift as soon as it lands.

Repro. A throwaway test in vendor/lock_inventory/tests.rs, run twice on 05ecc6e. One uv [[package]] unit whose pure wheel has no hash, followed by a hashed platform wheel:

wheels = [
    { url = "https://files.example/six-1.16.0-py3-none-any.whl" },
    { url = "https://files.example/six-1.16.0-cp312-cp312-manylinux_2_17_x86_64.whl", hash = "sha256:bbbb…" },
]
RECOVER:   Some(("https://files.example/six-1.16.0-py3-none-any.whl", "bbbb…"))   ← the platform wheel's digest
INVENTORY: resolved=None integrity=None                                          ← correctly no pure pinned wheel

The same test showed that pure_wheel_from_uv_unit also accepts cp311-none-any, which the shared rule will reject after #1053.

Symptoms

None filed. Impact: low severity and fail-closed. The fetch layer verifies the digest, so repair fails with a hash mismatch for the wrong wheel instead of the honest NO_URL / "no hash-pinned pure wheel" message. The structural cost is a third uv.lock reader, and portability drift between hosted/vendored and inventory/recovery once #1053 lands. The fixture is realistic for uv locks from --find-links or flat indexes, which carry no hashes.

Proposed change

  1. Add pub(crate) fn is_portable_wheel(file_name: &str) -> bool beside wheel_platform_from_filename, or use !wheel_platform_from_filename(..).0. Use it at the three inventory sites, stripping ?/# once in a shared helper.
  2. In recover.rs, parse the recorded uv_lock_package / pdm_lock_package fragment as TOML (it is a [[package]] unit) and reuse python_package_archive.
  3. Deleted: pure_wheel_from_uv_unit (about 25 lines) and the three ends_with("-none-any.whl") literals.

Size and scope

Acceptance criteria

  • The repro unit recovers to the NO_URL / "no hash-pinned pure wheel" error, not to a mismatched pair.
  • Inventory and recovery give the same answer for the same [[package]] unit. Add a table test over py3-none-any, py2.py3-none-any, cp311-none-any, *-abi3-*, a hashless pure wheel and a #sha256= URL.
  • After Fix interpreter-bound wheels treated as portable (#1048) #1053, an interpreter-bound wheel is non-portable in hosted, vendored, inventory and recovery alike.
  • Existing pure_wheel_rejects_short_hash_missing_hash_and_non_http_url, uv_lock_inventories_pure_wheels, uv_lock_one_line_wheels_array_pairs_the_pure_wheel_with_its_own_hash and poetry_lock_carries_the_pure_wheel_sha256_when_listed stay green.

Dependencies


Backlog review — 2026-10-08

Priority: P1 → P3. Digest verification rejects the mismatched recovery wheel. This is a fail-closed recovery/diagnostic issue, explicitly low severity; keep active #1121.

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

    agent:claimedagent:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)bugSomething isn't workingpm:uvuvpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions