On this page

Security advisories against committed lockfiles

A product compiles in what its committed Cargo.lock resolves: a TLS verifier, a root-certificate bundle, a parser. When a security advisory is published against one of those versions, every product that locks it keeps shipping the flaw until its lock moves. stado product advisories reads every committed lock of the workspace against a published advisory database, names each affected package version, and moves each one Cargo can fix without a manifest change.

stado product advisories list --advisory RUSTSEC-2026-0382
stado product advisories apply jeden --json
stado product advisories list --database https://osv-vulnerabilities.storage.googleapis.com

What it reads

stado product advisories list|apply [PRODUCT] [--advisory ID]... [--database ORIGIN] [--json]

  • Checkouts. PRODUCT reads the checkouts its catalogue record names (owner_repository and each surface); without it, every git checkout directly in the workspace (--workspace, else WISENT_WORKSPACE, else ~/Documents/CodingProjects/Wisent) is read. A product's checkout that is absent is said on standard error, checkout <path> of <repository> is absent, so what its lockfiles resolve is unknown, and fails the run.
  • Lockfiles. Every Cargo.lock the checkout's last commit holds (git ls-tree HEAD), at any depth, read as that commit holds it: a build of the committed source resolves the committed lock, and a lock rewritten on disk ships nothing until it is committed. A checkout with no commit holds none. A lockfile whose working file differs from its last commit (rewritten, staged or deleted) is named, <lockfile> differs from its last commit; the findings read the committed one, and is still read as committed. An untracked lock belongs to a build tree or a scratch export and is not read. Only packages a lock resolves from crates.io are judged; a git or path source is in no registry's advisory database.
  • Advisories. --database names an OSV database by its origin; Stado downloads <ORIGIN>/crates.io/all.zip, the whole crates.io archive the database's storage serves, once per run, and evaluates every range itself, so the number of packages in the workspace never meets a service's batch limit. The default is the public OSV database, https://osv-vulnerabilities.storage.googleapis.com, which carries the RustSec and GitHub advisories for crates. A withdrawn advisory affects no version and is judged against no lock; naming one with --advisory is refused by name. A range counted in a repository's commits rather than in versions is not read. A range's events are walked in version order as the schema's evaluation does; 0 is every version from the first, and a version written without its minor or patch (0.62) is the first version its Cargo requirement names.
  • Selection. --advisory ID keeps only the advisories it names, by id or by any alias, and is repeatable. Advisories published together often list each other as aliases, so naming one of them selects the whole group.

What it answers

For each affected package version of each committed lock, a finding: checkout, lockfile, package, version, advisory, aliases, summary, informational (RustSec's class when the advisory is a notice: unmaintained, unsound, notice), fixed (every version that closes an affected range), compatible_fix (the first fixed version above the locked one that is caret-compatible with it, so Cargo may lock it without a manifest change), dependents (the packages of the same lock that depend on this version, whose requirement must move when no compatible fix exists) and working (null while the lockfile's working file is its last commit's; otherwise the versions of the package the working file locks, empty when it locks none or was deleted). --json prints the checkouts read with their lockfiles and their uncommitted lockfiles, and the findings; without it, one line per update, one line per differing lockfile, one line per finding (ending its working file, not committed, locks <versions> when it differs) and a summary.

The exit status is a failure while a selected advisory that is not a notice still affects a committed lock, a product's checkout is absent, or an update apply asked for was not made. A fix locked on disk settles nothing until it is committed. A run with nothing standing exits 0.

What apply changes

apply groups the findings by affected package version and runs, for the advisories of each that have a compatible fix,

cargo update --manifest-path <the Cargo.toml beside the lock> --package <name>@<version> --precise <fix>

with the highest of those fixes, then reads every lock again. The answer adds updates: updated with the version it locked and the advisories that fix closes, moved when an earlier update of the same lock already moved this version (Cargo is not asked again; its cause names what the working lock holds now), refused when the lock may not be rewritten now, or failed when Cargo refused, each with its cause; and apart from them unfixed, naming the advisories of the same version that no caret-compatible version closes. An advisory without a compatible fix never holds back one that has one. An updated entry carries moved: every other package whose version or source the update changed in the lock, {package, from, to}, each version written as <version>, or <version> <source> for a package from anywhere but crates.io, since Cargo also moves what the fixed version requires (rustls-webpki 0.103.16 takes aws-lc-rs to 1.18 and aws-lc-sys with it); the line for a person ends cargo also moved <package> <from> -> <to>. findings are what stands in the committed locks after them, each naming what its rewritten working file now locks. Nothing is committed: the changed lockfiles are the checkout's to build, test and commit, so a change of the resolved graph is reviewed like any other change.

Every git command it runs, and Cargo, start without the variables git reads as naming one repository (git rev-parse --local-env-vars: GIT_DIR, GIT_INDEX_FILE, GIT_WORK_TREE and the rest), so a run from a git hook or with an index of its own reads each checkout as itself.

Refusals

Before anything is read:

  • the catalog at <catalog> names no product <PRODUCT>
  • --database "<ORIGIN>" is not a URL; give an https origin, and --database <ORIGIN> cannot carry a path
  • the advisory database <ORIGIN> did not answer <url>: <cause>. A certificate the client does not trust is named here: a host serving a distrusted chain reads invalid peer certificate: Other(OtherError(CertDistrusted)).
  • the advisory database <ORIGIN> answered <status> to <url>; an OSV database serves the crates.io advisories there
  • <url>: the advisory database <ORIGIN> answered with no zip archive, and <url>: <member> is not an OSV advisory
  • advisory <ID> names the version "<written>", which is not semver
  • the advisory database <ORIGIN> holds no crates.io advisory <ID>; name an id or alias it publishes
  • the advisory database <ORIGIN> withdrew <ID> at <time>: a withdrawn advisory affects no version, so there is nothing to read or lock for it; drop that --advisory, with <ID> (an alias of <advisory>) when it names the withdrawn advisory by an alias
  • git rev-parse HEAD in <checkout> failed (<status>): <git's error>, for a workspace entry that is no readable git checkout, and the failure of any other git command it runs, with git's words
  • git rev-parse --local-env-vars printed no UTF-8, and git <arguments> in <checkout> printed no UTF-8 (a committed lockfile that is not UTF-8 is no Cargo lockfile)
  • <lockfile> as last committed: not a Cargo lockfile: …, and <lockfile> as last committed: a [[package]] has no <name|version>; the same refusals name a differing working file by its path alone
  • <lockfile> is gone from the working tree, when apply finds the working lock it is rewriting deleted under it

An update's causes:

  • no fixed version of <package> is caret-compatible with <version> (fixed in: <versions>); the requirement of <dependents> on it must move in their manifests
  • no Cargo.toml beside <lockfile>: cargo update resolves a lockfile from the manifest beside it
  • <lockfile> has uncommitted changes (<git status>); commit or discard them first, so this update is the only change to it
  • an earlier update of this lockfile already moved <package> <version>; the working lockfile now locks <versions>, and the findings say whether that closes <advisories> (state moved)
  • cargo update refused <package> <from> -> <to>: <Cargo's error and its evidence directory>

Stado Desktop

The Products screen carries Read the published advisories that affect what each checkout's Cargo.lock resolves and Lock each advisory's first compatible fixed version in every checkout's Cargo.lock. They run stado product advisories list and apply on the selected Stado API host with the product, the advisories and the database the form names, and show the answer and its refusals as the CLI prints them; apply asks for confirmation first, and a finding that stands ends either operation as refused.

Real journey

tests/product/advisories/locks.mjs in Stado (STADO_BIN, the built Stado; Cargo on PATH) builds a workspace of git checkouts under .build, one locking webpki-roots 1.0.9, one pinning rustls-webpki =0.101.7 and one locking rustls-webpki 0.103.13 with aws-lc-rs 1.17.3, and runs every refusal above that needs no network fault (a withdrawn advisory among them), a list against the public OSV database, the same list with another repository's GIT_DIR and an index of its own in its environment answering the same, an apply refused on an uncommitted lock that leaves its bytes unchanged and names it as differing, an apply that locks 1.0.10 on disk and commits nothing while the committed finding stands and names 1.0.10 as what the working file locks, a list that keeps reading the committed lock until the fix is committed, an apply that names aws-lc-rs as moved beside rustls-webpki 0.103.16, the pinned one named as unfixed, and a list that settles once only fixed, committed checkouts remain.

Source: this website