Cargo Yank Case Study: Rust Deserts
Consider the my-private-registry
registry, which hosts the crate rust-deserts
.
rust-deserts
has published the following versions:
0.1.0
0.2.0
0.2.1
A second crate in the registry, innocent-bystander
, depends on rust-deserts
:
# Cargo.toml (innocent-bystander)
[dependencies]
rust-deserts = { version = "0.2.0", registry = "my-private-registry" }
A developer working on innocent-bystander
runs the cargo update
command while working on the crate, and Cargo identifies rust-deserts
version 0.2.1
as the highest available version based on the semantic versioning-based dependency resolution of the specifications declared in innocent-bystander
's Cargo.toml
.
The version of rust-deserts
used by innocent-bystander
is stored in its Cargo.lock
file:
# Cargo.lock (innocent-bystander)
[[package]]
name = "rust-deserts"
version = "0.2.1"
source = "registry+https://git.shipyard.rs/my-private-registry/crate-index.git"
checksum = "0b943c9c44efe45f8384871a022e4eb728cfed28a5e752fa57f7cb1c89680160"
A Dish Served Cold
Unfortunately, the authors of rust-deserts
had been rather cavalier in their use of unsafe
(infamously, the rationale for one unsafe
block included the line "a little ub never hurt anyone"), and discover that the changes in version 0.2.1
introduced a major security vulnerability.
To mitigate the security risk, rust-deserts
' authors yank version 0.2.1
:
$ # ~/rust-deserts
$ cargo yank --registry my-private-registry --version 0.2.1 rust-deserts
Since version 0.2.1
of rust-deserts
has been yanked, Cargo will never choose that version again when resolving dependencies. However, since 0.2.1
appears in the Cargo.lock
file for the innocent-bystander
crate, Cargo will still download the 0.2.1
version for it to use even after 0.2.1
has been yanked:
$ # ~/innocent-bystander - post-yank
$ cargo fetch
Updating `my-private-registry` index
Downloaded rust-deserts v0.2.1 (registry `my-private-registry`)
Downloaded 1 crate (680 B) in 0.01s
However, if the cargo update
command is run for innocent-bystander
, the rust-deserts
dependency will be downgraded down to 0.2.0
(which is not yanked).
$ # ~/innocent-bystander
$ cargo update
Updating `my-private-registry` index
Updating rust-deserts v0.2.1 (registry `my-private-registry`) -> v0.2.0
The same is true if the Cargo.lock
file is simply deleted from the directory, prompting Cargo to re-resolve the dependency graph.
$ # ~/innocent-bystander
$ rm Cargo.lock
$ cargo fetch
Updating `my-private-registry` index
Downloaded rust-deserts v0.2.0 (registry `my-private-registry`)
Downloaded 1 crate (678 B) in 0.01s
A Harder Case: Patch vs. Minor Version
Life goes on, and rust-deserts
reaches a v1.0
release. The crate has now published:
0.1.0
0.2.0
0.2.1
1.0.0
1.1.0
Alas, the 1.1.0
release includes another jaw-dropping security vulnerability, and innocent-bystander
is caught up in the problem once again:
# Cargo.toml (innocent-bystander)
[dependencies]
rust-deserts = { version = "1.1.0", registry = "my-private-registry" }
Like before, innocent-bystander
can still download version 1.1.0
of rust-deserts
so long as it appears in its Cargo.lock
file. However, this time the cargo update
command results in a failed build, due to the semantic version-based dependency resolution of Cargo:
$ # ~/innocent-bystander
$ cargo update
Updating `my-private-registry` index
error: failed to select a version for the requirement `rust-deserts = "^1.1.0"`
candidate versions found which didn't match: 1.0.0, 0.2.0, 0.1.0
location searched: `my-private-registry` index
required by package `innocent-bystander v0.1.0 (~/innocent-bystander)`
Cargo's dependency resolution rules allow downgrading from 0.2.1
to 0.2.0
, since they are only a patch version away from each other.
But 1.1.0
and 1.1.0
are a minor version apart, and the Cargo.toml
specifies the dependency to minor version granularity, preventing an automatic downgrade. This is just the reverse of the normal upgrade process performed during cargo update
.
If innocent-bystander
had specified a major version only, i.e.:
# Cargo.toml (innocent-bystander)
[dependencies]
rust-deserts = { version = "1", registry = "my-private-registry" }
...it could still downgrade from 1.1.0
to 1.0.0
, even if it had previously used 1.1.0
:
$ # ~/innocent-bystander - with `version = "1"` (vs "1.1.0")
$ cargo update
Updating `my-private-registry` index
Updating rust-deserts v1.1.0 (registry `my-private-registry`) -> v1.0.0