plugin update compares no version: an OLDER package auto-applies as an update (measured, 0.0.1 over 9.9.9) #255
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#255
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What was measured
code-index plugin updateapplied version 0.0.1 over an installed 9.9.9 of the same package, printedapplied, and made the older digest the live one. Every one of #237's authority conditions was satisfied, because none of them is about the version.Black box: shipped binaries only (
target/debugatee39fd1), an operator-generated publisher key viaplugin key generate/plugin sign/plugin trust add, andpython3 -m http.serveras the source. No test harness, no fixtures crate.Repro
Two copies of
tests/packages/xaml, identical but for[package] version:Note the two
extraction_identityvalues are equal —package.versionis excluded from it by design — so the cost clause does not fire either.Install 9.9.9, then configure the older one as the update source:
code-index plugin update:code-index plugin statusafterwards:The 0.0.1 digest is live.
Why every condition passed
crates/indexer/src/update.rs:283-485compares package id, publisher fingerprint, granted capabilities/bridges/derived_names(subset),[[displaces]]rows, thedeclinedtombstone, andextraction_identity. There is no ordering or monotonicity comparison anywhere, andupdate_one's only identity short-circuit isif candidate.digest == parsed_current(crates/cli/src/plugin.rs:3866) — equal, not newer. A downgrade differs from an upgrade in exactly the field nothing reads.Why it matters
UpdateConfigholds no digest pin, by design (crates/indexer/src/config.rs:150-160: a pin "would have to be edited on every release"), and its own doc already accepts that "a VALIDLY SIGNED BUT DIFFERENT package can be served … the set a server may substitute from is one publisher's, and every member of it is then held to conditions 2 through 4". The measurement above is that conditions 2-4 do not exclude an earlier member of that set. So anyone who can choose the bytes at the configured URL — the publisher, a compromised release pipeline, or a network position, since the transport is trusted for nothing here — can roll a project back to a superseded version of the same package with the same grant. That is the one substitution the design says the authority clauses catch, and it is not caught.auto_apply = false(the default) is unaffected in practice: a human reads the line, and the line does print(version 0.0.1). The exposure isauto_apply = true, where by construction nobody reads it. The version is disclosed but not graded.What is not claimed
This is not a signature or trust bypass: the bytes are genuinely signed by the anchored publisher. It is that "nothing about the authority changed" is currently true of a downgrade.
Suggested shape
One clause beside the others in
assess_auto_apply, with its own reason code (update_version_not_newer), comparingcandidate.versiontocurrent.version. Two things worth deciding explicitly rather than by default:Errarm is where this class of clause usually leaks;80-operator-recovery.md§9 says so). That is arguably a refusal too, and is a separate decision from ordering.Per the repo's "generic mechanisms, not a fix per case" rule this is one clause resting on a structural fact (versions are ordered), not a special case.
Test gap it sits in
crates/cli/tests/plugin_update_cli.rshas 15 tests and a refusal arm for every other compared field. There is no version test because there is no version clause. A fix needs one, plus the mutation showing it can fail.Found by Lane C while walking the operator install/update path against a published release.