Open Source Security

Auditing Rust unsafe Code at Scale

How to actually audit unsafe blocks across a large Rust dependency graph without drowning in false positives or miss real issues.

Nayan Dey
Senior Security Engineer
7 min read

"We use Rust, so we are memory-safe" is a claim I have heard in more vendor presentations than I can count. It is a half-truth. The Rust compiler guarantees memory safety in the absence of unsafe. In the presence of unsafe, it guarantees nothing beyond what the author of the unsafe block reasoned about. Any real-world Rust application has unsafe code in it, usually transitively through dependencies. Auditing that unsafe code is tractable at a small scale and genuinely hard at scale. This is the playbook I have converged on over the past eighteen months.

Why Auditing unsafe Is Not Optional

The RustSec database has cataloged dozens of soundness-related advisories against popular crates through 2023 and 2024. RUSTSEC-2024-0019 (mio 0.8.10 on Windows) was an unsoundness in an unsafe block that held a socket pointer across a drop boundary. RUSTSEC-2024-0332 (against a smaller but widely-used crate) stemmed from a Send/Sync implementation that turned out to be incorrect under concurrent access. These are not hypothetical. They are real, recent, and they would have been catchable by someone who knew what to look for.

The scope is real too. A modest Rust project I looked at in August 2024 had 2,847 unsafe blocks across its direct and transitive dependencies. Not all of them are interesting; most are wrappers around libc calls or FFI shims. But the distribution is long-tailed and a small number of them do non-trivial unsafe reasoning. Finding those is the job.

Start With cargo-geiger

cargo-geiger 0.11.7 (as of late 2024) is the first pass. It counts unsafe blocks per crate and per function, distinguishes between code the crate authored versus code it pulled in, and gives you a per-dependency summary. It is not a vulnerability scanner; it is a map. The output tells you which crates to look at, not what is wrong with them.

The rough heuristic I use from the cargo-geiger output: any dependency in the top five percent of unsafe density is worth a look. Any dependency that has unsafe code and is under 500 stars on GitHub or has not been updated in two years is worth a longer look. Any dependency that declares #![deny(unsafe_code)] at its crate root and still somehow has unsafe is either a false positive from the tool or a wrapping-of-dependency pattern that deserves explanation.

Then Look At What Unsafe Is Doing

Unsafe blocks broadly fall into categories that require different audit depth.

FFI wrappers: libc calls, system calls, bindings to C libraries. These are unsafe because the function signatures are unsafe by definition, but the Rust-level reasoning is usually "the caller ensured arguments are valid." Read the safety comment; confirm callers respect the invariants; move on.

Pointer arithmetic and raw pointers: this is where soundness bugs live. Look for unsafe { std::ptr::read(...) }, ptr.offset(n), slice::from_raw_parts. The audit question is whether the lifetime, alignment, and initialization preconditions are met. This requires reading the surrounding code carefully.

Send/Sync impls: unsafe impl Send for Foo {} is declaring that the type is safe to move across threads. If the type contains raw pointers, interior mutability, or references to thread-local state, this is a real correctness claim the author is making. These are surprisingly common and surprisingly often wrong.

Transmute: std::mem::transmute is the easiest way to create UB in Rust. It should be rare and every instance should have a paragraph-length safety comment. If you see transmute without a comment, flag it.

Inline asm: Rust 2021 stabilized asm! and it is increasingly used in performance-critical crates. The safety reasoning is domain-specific and frequently requires platform expertise to audit.

miri Is The Dynamic Counterpart

Miri, the interpreter for Rust's MIR, detects undefined behavior at runtime. Running your test suite under miri will catch some categories of UB that static review misses. The trade-off is speed: miri is several orders of magnitude slower than native execution, so you typically run it against a subset of tests on a schedule rather than on every PR.

In my 2024 workflow I run cargo miri test against a designated miri-safe subset of tests nightly. Over a year of doing this I found three genuine issues in dependencies I had not written, two of which resulted in upstream fixes. The false positive rate is low if you stay off code that does inline assembly or deep FFI.

A limitation to be aware of: miri does not run code that calls into non-Rust libraries. If your test path goes through openssl-sys or similar, miri will bail. The tests that miri can meaningfully run tend to be the more algorithmic ones, which is fine because that is also where unsafe pointer bugs live.

Cargo-vet For Shared Audit Effort

Cargo-vet, published by Mozilla and stabilized through 2023 and 2024, lets you record audit decisions against specific crate versions and share them across projects or organizations. You record "I reviewed serde 1.0.210 and it is fine" and the tool remembers so you do not re-audit the same version.

The interesting pattern is the audit import. Mozilla, Google, Embark, and others publish their audits under open licenses. You can import their audits and only audit the delta: crates they have not reviewed, or versions newer than the ones they did. This turns a hopeless workload into a manageable one. My current workspace imports Mozilla's and Google's audits and I audit the delta, which is usually one or two dozen crates for a significant release.

What Actually Finds Bugs

After running this playbook for most of 2024, here is the distribution of where I actually found problems:

Manual review of unsafe blocks in crates with low maintenance activity: most of the real findings. The common pattern is an unsafe impl of Send or Sync that was written when the type was simpler and is now incorrect because a field was added.

miri nightly runs: occasional finds, usually in crates doing interior mutability tricks.

cargo-geiger triage: no direct finds but invaluable for prioritizing where to look.

Dependency version bumps that introduced new unsafe: caught two cases where a crate added a fast-path unsafe implementation in a minor release. Both were sound, but in one case the safety comment was missing, which I reported upstream.

The failure mode I want to flag explicitly: auditing unsafe is fatiguing. After the first hundred unsafe blocks your attention drops. Real review requires pacing and probably rotation across reviewers. Do not schedule eight-hour audit sprints; schedule two-hour focused sessions and come back.

The Delta Review Discipline

The highest leverage practice I have adopted is the delta review. When a dependency version bumps, I look at just the unsafe-block diff between the old and new version. Most of the time there is no diff. Sometimes there is a diff and it is trivial. Occasionally it is the real work. But looking at only the diff, not re-auditing from scratch, keeps the workload bounded.

Tooling for this is still rough. I have a small script that runs git diff between two crate versions filtering for unsafe keyword changes. A better tool would integrate with cargo-vet so the audited version is the baseline. This is on my list for 2025.

How Safeguard Helps

Safeguard tracks unsafe-block surface area per crate version in your graph and highlights the delta between lockfile updates, so you are reviewing the few hundred lines that changed rather than the ten thousand lines that did not. We integrate cargo-vet audit imports from major published sources and layer your own audit decisions on top, which collapses the re-audit problem when you bump versions. We also correlate unsafe density with maintainer activity and historical soundness advisories, surfacing the dependencies most likely to hide the next issue rather than treating all unsafe as equal.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.