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