A Rust project rarely stands alone. Cargo makes it so easy to add a library that a small command-line tool can pull in dozens of crates written by people you have never heard of, and each of them runs with the same rights as your own code. This page explains how dependencies are chosen and pinned, where code runs that you may not expect, what has actually gone wrong on crates.io, and what you can do about it. The general idea of a supply chain attack is on the Security pages; the basics of crates, packages and Cargo.toml are on Functions, Modules and Crates and Installing Rust and Using Cargo.

The examples on this page run without network access, so they use path dependencies (a crate in a neighboring folder) instead of downloads from crates.io. Everything that involves crates.io itself is shown as illustrative output.

crates.io, the public registry

crates.io is the default package registry for Rust: the public site where crates are published and from which Cargo downloads them. It has been operated by the Rust Foundation since 2021, when Mozilla transferred it together with the project's other infrastructure. Anyone with an account can publish a crate under a name that is still free; there is no review before publication. A dependency on a crates.io crate is one line in Cargo.toml, usually added with cargo add:

cargo search tariff          # search crates.io by name and description
cargo add serde --features derive   # adds a line to [dependencies]
[package]
name = "kestrel-readings"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { version = "1.0", features = ["derive"] }

Two properties of crates.io matter for safety. First, a published version is permanent: in the words of the Cargo book, "the version can never be overwritten, and the code cannot be deleted". What you reviewed under a version number stays exactly that code. There are narrow exceptions (Rust RFC 3660): owners may delete a crate shortly after publishing it (within 72 hours), or under strict conditions for crates that almost nobody uses, and the crates.io team removes malware. Second, a publish includes the source code, so documentation on docs.rs and the code you download match what the author uploaded, not what is in their Git repository. The two can differ.

Version requirements

The version in a dependency line is not one fixed version but a version requirement: a rule that says which versions are acceptable. Cargo assumes that crates follow semantic versioning, and a plain version number is a caret requirement: any version that is compatible according to semver, starting at the one given.

Requirement Accepts Note
"1.2.3" = "^1.2.3" ≥ 1.2.3 and < 2.0.0 the default: compatible updates
"1.2" ≥ 1.2.0 and < 2.0.0 missing parts count as 0
"0.3.10" ≥ 0.3.10 and < 0.4.0 before 1.0, the second number is the "major" one
"~1.2.3" ≥ 1.2.3 and < 1.3.0 tilde: patch updates only
"=1.2.3" exactly 1.2.3 rarely needed; blocks fixes too
">=1.2, <1.5" the range given comparison requirements, combined with a comma
"*" any version not allowed for crates published on crates.io

Caret requirements are what lets the ecosystem receive bug fixes and security fixes without every user editing every Cargo.toml. They also mean that a new release of any crate in the tree, by its author or by someone who has taken over the author's account, can end up in your next build that resolves versions afresh. That is the problem Cargo.lock solves.

How Cargo picks a version

When a build needs a dependency that is not yet locked, Cargo resolves it: it picks the highest version that matches the requirement, and does the same for that crate's own dependencies (the transitive dependencies). Since Rust 1.84 the resolver also takes the rust-version field of each crate into account, and prefers versions that work with your toolchain; for projects on the 2024 edition this is the default, which explains the "highest Rust 1.99.0 compatible version" in the output. Here a binary crate depends on the p1crc library from Testing, Docs and Tooling (a prototype in this example) through a path dependency with a version requirement:

cargo new -q --vcs none --lib p1crc
cat > p1crc/src/lib.rs <<'EOF'
/// CRC-16/ARC: reflected polynomial 0xA001, start value 0, no final XOR.
pub fn crc16_arc(data: &[u8]) -> u16 {
    let mut crc: u16 = 0;
    for &byte in data {
        crc ^= u16::from(byte);
        for _ in 0..8 {
            crc = if crc & 1 != 0 { (crc >> 1) ^ 0xA001 } else { crc >> 1 };
        }
    }
    crc
}
EOF
cargo new -q --vcs none kestrel-readings
cd kestrel-readings
echo 'p1crc = { path = "../p1crc", version = "0.1" }' >> Cargo.toml
cat > src/main.rs <<'EOF'
fn main() {
    println!("{:04X}", p1crc::crc16_arc(b"123456789"));
}
EOF
cargo run
cargo tree
     Locking 1 package to highest Rust 1.99.0 compatible version
   Compiling p1crc v0.1.0 (/home/amara/p1crc)
   Compiling kestrel-readings v0.1.0 (/home/amara/kestrel-readings)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/kestrel-readings`
BB3D
kestrel-readings v0.1.0 (/home/amara/kestrel-readings)
└── p1crc v0.1.0 (/home/amara/p1crc)

cargo tree, built into Cargo, shows the whole dependency tree; cargo tree -i p1crc shows why a crate is in it ("inverted"), and cargo tree -d lists crates that appear in several versions. In a real project the tree is usually much longer than the list in Cargo.toml. A requirement that no available version meets stops the build before anything is compiled. Change the requirement to "0.2" while p1crc is still 0.1.0:

cargo new -q --vcs none --lib p1crc
cargo new -q --vcs none kestrel-readings
cd kestrel-readings
echo 'p1crc = { path = "../p1crc", version = "0.2" }' >> Cargo.toml
cargo build
error: failed to select a version for the requirement `p1crc = "^0.2"`
candidate versions found which didn't match: 0.1.0
location searched: /home/amara/p1crc
required by package `kestrel-readings v0.1.0 (/home/amara/kestrel-readings)`
note: offline mode (via `--offline`) can sometimes cause surprising resolution failures
help: if this error is too confusing you may wish to retry without `--offline`

Cargo shows the requirement as ^0.2: the caret is implied, and 0.1.0 is not compatible with 0.2 because, before 1.0, a change in the second number counts as a breaking change. The two last lines appear because this site's test machine runs Cargo in offline mode.

Cargo.lock

Cargo.lock is the file in which Cargo records the exact result of resolving: every package in the tree with its exact version, its source and, for downloaded crates, a checksum of the package file. Cargo creates it on the first build and from then on builds exactly those versions, until you change Cargo.toml or run cargo update. It plays the same role as package-lock.json for npm (see Modules and npm).

cargo new -q --vcs none --lib p1crc
cargo new -q --vcs none kestrel-readings
cd kestrel-readings
echo 'p1crc = { path = "../p1crc", version = "0.1" }' >> Cargo.toml
cargo build -q
cat Cargo.lock
sed -i 's/^version = "0.1.0"/version = "0.1.1"/' ../p1crc/Cargo.toml   # a new release of p1crc
cargo build --locked || echo "exit status: $?"
cargo update
grep -A1 'name = "p1crc"' Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4

[[package]]
name = "kestrel-readings"
version = "0.1.0"
dependencies = [
 "p1crc",
]

[[package]]
name = "p1crc"
version = "0.1.0"
error: cannot update the lock file /home/amara/kestrel-readings/Cargo.lock because --locked was passed to prevent this
help: to generate the lock file without accessing the network, remove the --locked flag and use --offline instead.
exit status: 101
     Locking 1 package to highest Rust 1.99.0 compatible version
    Updating p1crc v0.1.0 (/home/amara/p1crc) -> v0.1.1
name = "p1crc"
version = "0.1.1"

After p1crc moves to 0.1.1, the lock file no longer matches. cargo build --locked refuses to change it and fails with exit status 101 (its help: line mentions --offline because the test machine has no network). Only cargo update moves the project to the new version. (A plain cargo build would also rewrite the lock file here, but only because p1crc is a path dependency whose own manifest changed; for a crates.io dependency an ordinary build keeps the locked version until you run cargo update or change Cargo.toml.) The change then shows up as a diff of Cargo.lock in version control. A package from crates.io has two more lines in the lock file:

[[package]]
name = "serde"
version = "1.0.…"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "…64 hexadecimal digits: the SHA-256 hash of the downloaded .crate file…"

The checksum means a build fails if the package file that arrives differs from the one that was locked. Together with the rule that published versions never change, a committed Cargo.lock makes builds reproducible, and it makes dependency changes deliberate and reviewable.

Yanked versions

Because published code cannot be deleted, crates.io offers a softer tool. A yanked version is a published version that its owner has marked as "do not use for new projects". Cargo no longer picks it when it resolves versions, but, as the Cargo book puts it, "all existing dependencies continue to work": a project whose Cargo.lock already names the version keeps building it. Owners yank a release that is broken or that has a serious bug, and publish a fixed version.

cargo yank --version 0.1.1        # mark p1crc 0.1.1 as yanked (owners only)
cargo yank --version 0.1.1 --undo # reverse it

cargo package --list shows exactly which files a publish would upload. Here a test file with a telegram from a pilot home (in this example) would have gone to crates.io, permanently, until an exclude entry keeps it out (2>/dev/null hides a warning about missing package metadata):

cargo new -q --vcs none --lib p1crc
cd p1crc
mkdir testdata
echo '/KST5' > testdata/telegram-h0017.txt
cargo package --list 2>/dev/null
sed -i 's/^edition = "2024"/edition = "2024"\nexclude = ["testdata\/"]/' Cargo.toml
echo "--- with exclude:"
cargo package --list 2>/dev/null
Cargo.lock
Cargo.toml
Cargo.toml.orig
src/lib.rs
testdata/telegram-h0017.txt
--- with exclude:
Cargo.lock
Cargo.toml
Cargo.toml.orig
src/lib.rs

Cargo.toml.orig is the manifest as you wrote it; Cargo publishes a normalized Cargo.toml next to it.

For users, a yanked version in the lock file is a signal to look at: cargo-audit reports yanked crates as warnings (see below).

Build scripts and procedural macros

A build script is a file build.rs in a package's root folder. Cargo compiles it and runs it on the build machine before compiling the package itself. Build scripts exist for good reasons: to compile a bundled C library (see Unsafe Rust and Calling C), to find a system library, or to generate code. They talk to Cargo by printing lines that start with cargo:::

cargo new -q --vcs none meter-report
cd meter-report
cat > build.rs <<'EOF'
fn main() {
    // Runs on the build machine, before the crate itself is compiled.
    println!("cargo::rerun-if-changed=build.rs");
    println!("cargo::rustc-env=KESTREL_BUILD=pilot");
    println!("cargo::warning=build script ran");
}
EOF
cat > src/main.rs <<'EOF'
fn main() {
    println!("build: {}", env!("KESTREL_BUILD"));
}
EOF
cargo run
cargo run
   Compiling meter-report v0.1.0 (/home/amara/meter-report)
warning: meter-report@0.1.0: build script ran
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/meter-report`
build: pilot
warning: meter-report@0.1.0: build script ran
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/meter-report`
build: pilot

The build script set an environment variable that the env! macro read while compiling, and printed a warning that Cargo shows. On the second cargo run nothing was compiled or run again (there is no Compiling line): Cargo repeated the stored warning, and rerun-if-changed tells it to run the script again only when build.rs changes.

The important part is what this means for dependencies. A build script is an ordinary Rust program that runs with your user's rights: it can read files in your home folder, read environment variables (including tokens), and use the network. Procedural macros (the code behind #[derive(...)] attributes from crates and similar macros) are the same: they are compiled to a library that runs inside the compiler. Both run during cargo build, cargo check, cargo test and, in many editors, as soon as rust-analyzer opens the project. A malicious dependency does not need you to run your program.

A dependency tree with invented crate names. The binary kestrel-readings, your own code, depends on three crates: p1crc through a path, telegram-parse 2.1.0 from crates.io, and derive-helper 1.0.4, a procedural macro. telegram-parse has a build script and a dependency of its own, crc-tables 0.3.1. The build script and the procedural macro run on the build machine during cargo build; Cargo.lock pins all versions. kestrel-readings your code p1crc 0.1.0 path dependency telegram-parse 2.1.0 crates.io derive-helper 1.0.4 proc macro: runs in rustc build.rs runs at build time crc-tables 0.3.1 transitive Cargo.lock pins every version in this tree, with checksums for downloaded crates
Code from dependencies runs at two moments: build scripts and procedural macros during the build, everything else when your program runs. Crate names are invented.

Reading a dependency's build.rs before you add it is a small effort with a large effect: most are a few lines that look for a library or compile some C, and anything that downloads files, reads your home folder or runs shell commands deserves questions. cargo vendor (built in) copies the source of every dependency into your project, which makes such a review, and offline builds, easier. On a pipeline, run builds in a container or runner without access to secrets they do not need.

Supply-chain attacks on crates.io

Memory safety protects a program against its own mistakes, not against code that is written to do harm. The incidents below are described at the level of the official announcements on the Rust blog, with their dates. They show the three ways in which malicious code has reached Rust developers: a confusingly similar name, a taken-over account, and attacks on the maintainers themselves.

Typosquatting

Typosquatting is publishing a package under a name that looks like that of a popular one, so that a typing error, a copied wrong name or a guess installs it instead. On 24 September 2025 the crates.io team removed two such crates, faster_log and async_println, published on 25 May 2025 and downloaded 7,181 and 1,243 times. According to the announcement, they copied the code and documentation of a legitimate logging crate, worked as a logger, and in addition searched the log files they processed for cryptocurrency private keys and sent what they found to the attacker. The malicious code ran when a program or its tests ran, not at build time. Similar crates impersonating other crates were removed in December 2025 and February 2026.

Since 13 February 2026 the crates.io team no longer writes a blog post for every malicious crate, because most had no real users; it promises to "always publish a RustSec advisory when a crate is removed for containing malware", for example RUSTSEC-2026-0010 and RUSTSEC-2026-0011. Malware with real usage still gets a blog post.

A hijacked crate: arrayref (August 2026)

On 20 August 2026 at 07:15 UTC the Rust Security Response Team received a report that a crate called proc-macro1 was malicious: according to the announcement, "the crate had a build script that was downloading a malicious payload". The team then found that the popular crate arrayref had just been republished as version 0.3.10 with a dependency on it, and the most recent legitimate versions had been yanked. Two other crates of the same author, internment 0.8.7 and append-only-vec 0.1.9, were affected in the same way. The malicious versions were online for 86, 90 and 107 minutes. The team deleted them, reversed the yanks and locked the account; the post states that the author was not believed to be acting maliciously, but that "their computer or credentials are likely compromised".

Two lessons follow directly from the mechanics on this page. The payload sat in a build script, so building a project was enough to run it. And the new release only reached projects that resolved versions during that window: a project building from a committed Cargo.lock with --locked kept the version it had locked. The announcement told users how to check whether they had downloaded one of the versions: by looking for the package files in Cargo's download cache, ~/.cargo/registry/cache.

Attacks on maintainers

On 17 September 2026 the crates.io team and the security response working group warned of "an ongoing campaign targeting rust-lang members and owners of popular crates that is attempting to compromise devices and accounts in order to use them to publish malware". A video call is set up for something attractive, a job, a project or a contract, and used to get the target to install software (a "missing audio codec") or run a command. The post says this attack style "is known to be used by the DPRK" and links the arrayref compromise to similar attacks. Its advice: be suspicious of cold outreach, set up calls yourself on a platform you already use, and check that your accounts have MFA enabled and show no unexpected logins (see Social Engineering and Phishing and two-factor authentication).

For people who publish crates, crates.io offers Trusted Publishing (announced in July 2025, at first for GitHub Actions): a GitHub Actions workflow proves its identity with OpenID Connect and receives a short-lived token, so no long-lived crates.io API token has to be stored as a secret, or can be stolen from a laptop.

Bugs in Cargo itself

The tools are software too. In 2026 the Rust Security Response Team published three advisories for Cargo. CVE-2026-33056 was a vulnerability in the third-party tar crate that Cargo uses to unpack packages, which let a malicious crate change the permissions of arbitrary directories while it was extracted; crates.io blocked such uploads on 13 March and checked every crate ever published, and Rust 1.94.1 (26 March 2026) fixed Cargo. CVE-2026-5222 (low severity) and CVE-2026-5223 (medium, for third-party registries only; crates.io was not affected) concerned third-party registries and were fixed in Rust 1.96.0 (28 May 2026). Keeping the toolchain up to date with rustup update is part of supply-chain hygiene; as of October 2026 the current stable release, 1.99.0, contains all three fixes. See CVE: a name for each vulnerability for how such identifiers work.

Advisories and audits

The RustSec Advisory Database is the public database of security advisories for Rust crates, maintained by the Rust Secure Code Working Group: vulnerabilities, malicious crates and crates that are no longer maintained, each with an ID such as RUSTSEC-2022-0051, the affected and patched versions, and often a CVE number. It is open data in a Git repository; GitHub's advisory database includes its advisories, so Dependabot alerts cover Rust projects as well.

cargo-audit is the RustSec project's command-line tool that checks the exact versions in a Cargo.lock against the database and reports every match, plus yanked versions as warnings. It is installed separately and needs network access to fetch the database, so it is not available on this site's test machine. A report looks like this (shortened; the fields follow the example on rustsec.org):

cargo install cargo-audit --locked
cargo audit
    Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
    Scanning Cargo.lock for vulnerabilities
Crate:     lz4-sys
Version:   1.9.3
Title:     Memory corruption in liblz4
ID:        RUSTSEC-2022-0051
Solution:  Upgrade to >=1.9.4
error: 1 vulnerability found!

The command exits with a non-zero status when it finds a vulnerability, so it can fail a pipeline, and many projects also run it on a schedule, because a new advisory can appear for a lock file that has not changed. This is software composition analysis for Rust. Two more tools go further:

  • cargo-deny checks advisories too, and in addition enforces a policy written in a deny.toml file: which licenses are allowed, which crates are banned or may only appear in one version, and which registries and Git sources are trusted (as of October 2026, version 0.20).
  • cargo vet and cargo-crev record who has reviewed which crate versions, so a team (or a community) can share the work of reading dependencies.
[licenses]
allow = ["MIT", "Apache-2.0"]

[bans]
multiple-versions = "warn"

[sources]
unknown-registry = "deny"
unknown-git = "deny"

A software bill of materials (SBOM) answers the question "which crate versions are in this binary?" after the fact, for example when a new advisory appears; Cargo.lock is already most of that list, and tools such as cargo-cyclonedx produce a standard SBOM from it.

A dependency policy for a small team

In this example, Kestrel's platform team writes down a short policy before the first internal Rust tool, the p1crc prototype, gets dependencies. It is a proposal built from the mechanics and incidents on this page, not an existing Kestrel rule:

  1. Commit Cargo.lock and build, test and release with --locked. Dependency updates are separate pull requests whose Cargo.lock diff is reviewed.
  2. Pin the toolchain with a rust-toolchain.toml file, so every developer and the pipeline use the same compiler, Clippy and rustfmt, and update it deliberately (for example soon after each six-weekly release).
  3. Think before adding a crate. Does the standard library already do it? Is it maintained, widely used, with more than one owner? Read its build.rs and check whether it pulls in procedural macros.
  4. Audit continuously: cargo-audit or cargo-deny in the pipeline and on a schedule; treat a RustSec advisory like any other vulnerability.
  5. Protect the build: pipelines get only the secrets they need; anyone who publishes crates uses MFA and Trusted Publishing instead of a long-lived token.
[toolchain]
channel = "1.99.0"
components = ["clippy", "rustfmt"]

With this file in the project root, rustup uses Rust 1.99.0 for every cargo and rustc command in that folder, whatever the default toolchain is. Pinning has a cost: the pinned version stops receiving fixes, so the policy only works when someone moves it forward regularly. The same reasoning applies to firmware: the Kestrel Home dongle's firmware is written in C by an outside manufacturer, and the questions about its components belong in the supplier discussion on IoT Security.