How to Audit Unsafe Code in Rust Dependencies (cargo-audit, cargo-geiger)

Install and run cargo-audit and cargo-geiger to scan Rust dependencies for security vulnerabilities and unsafe code.

When dependencies bite back

You just added a popular crate to your project. It compiles. The tests pass. You feel good. Then a security advisory drops. The crate has a buffer overflow in a rarely used function. Or worse, the crate uses unsafe to bypass checks, and a subtle bug means your data is corrupted in production. Crates.io is a treasure trove, but it is not a magic safety filter. You need tools to look under the hood before you trust the code.

Rust gives you memory safety guarantees, but those guarantees only hold if the code follows the rules. unsafe blocks are where the rules get suspended. A dependency might use unsafe correctly, or it might use it incorrectly. You cannot read every line of every dependency. You need a way to measure risk. cargo audit checks for known security holes. cargo geiger measures how much unsafe code is lurking in your dependency tree. Think of cargo audit as checking the police report for a neighborhood. Think of cargo geiger as walking through the neighborhood with a radiation detector. One tells you about confirmed crimes. The other tells you where the hazardous materials are stored.

The two tools in your belt

cargo audit and cargo geiger solve different problems. They are not interchangeable. cargo audit queries a database of known vulnerabilities. It tells you if a specific version of a crate has a CVE or a RustSec advisory. cargo geiger analyzes the source code of your dependencies. It counts unsafe blocks, extern blocks, and raw pointer usage. It gives you a heuristic for how much trust you are placing in the crate authors.

Install both tools globally so you can run them in any project. The tools are written in Rust and compile quickly.

# Install the tools globally so you can run them in any project
cargo install cargo-audit cargo-geiger

# Run audit to check for known vulnerabilities in Cargo.lock
cargo audit

# Run geiger to scan for unsafe code usage
cargo geiger

cargo audit reads your Cargo.lock file. It compares the exact versions of your dependencies against the RustSec advisory database. If you do not have a Cargo.lock, it checks the versions Cargo would resolve from your Cargo.toml. cargo geiger parses the source code of every crate in your dependency graph. It outputs a report listing crates and the number of unsafe constructs found.

The compiler enforces memory safety, not security policy. It will compile a vulnerable crate without a single warning. You need cargo audit to bridge that gap.

How cargo audit works

cargo audit fetches the latest advisory database from the RustSec repository. This database is community maintained. When a vulnerability is discovered in a crate, the maintainers or the community submit an advisory. The advisory includes the affected versions, the severity, a description, and a link to the fix.

When you run cargo audit, the tool downloads the database if it is older than a day. It then iterates through every crate in your Cargo.lock. For each crate, it checks if the version matches any advisory. If it finds a match, it prints a warning with the advisory ID, the severity, and a recommendation.

warning: Vulnerability found in serde 1.0.100
id: RUSTSEC-2021-0001
url: https://rustsec.org/advisories/RUSTSEC-2021-0001
title: Denial of service in serde
severity: High
date: 2021-01-01
patched_versions: >= 1.0.101

The tool supports several flags that matter for real workflows. Use --ignore to suppress false positives. Sometimes an advisory does not apply to your use case, or the crate has been forked and fixed. Use --deny to set a policy. If you run cargo audit --deny warnings, the tool exits with a non-zero status code if any vulnerabilities are found. This makes it easy to integrate into CI pipelines.

Convention aside: cargo audit is often part of CI pipelines. Add a step that runs cargo audit --deny warnings to block builds with unpatched CVEs. This prevents vulnerable code from reaching production.

# .github/workflows/audit.yml
name: Security Audit
on: [push]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install cargo-audit
        run: cargo install cargo-audit
      - name: Run audit
        # Fail the build if any vulnerabilities are found
        run: cargo audit --deny warnings

Audit only knows about reported vulnerabilities. Zero-days will not show up. The tool is a safety net, not a guarantee. Run cargo audit fetch to manually refresh the database if you suspect it is stale.

How cargo geiger works

cargo geiger does not check a database. It reads the code. It parses the abstract syntax tree of every dependency and counts unsafe constructs. The output is a table with columns for unsafe blocks, extern blocks, and raw pointers.

# cargo geiger output
Crate                    Unsafe  Extern  Raw Ptrs
serde                    2       0       0
hyper-fast-parsing       45      2       12
tokio                    12      0       4

The numbers are heuristics. A high count of unsafe blocks does not mean the code is broken. It means the crate relies on low-level operations that the compiler cannot verify. The standard library uses unsafe to provide safe abstractions. A crate like ring uses unsafe extensively but is carefully audited and considered safe. cargo geiger flags ring because it has many unsafe blocks. It does not know that ring is audited.

extern blocks indicate FFI calls. If a crate calls C code, that is a trust boundary. The Rust safety guarantees do not extend to C code. If a crate has extern blocks, you are trusting the C library as well. Raw pointers indicate manual memory management or FFI. They are often used inside unsafe blocks.

Convention aside: cargo geiger has a --no-dev flag. Use it to ignore development dependencies. Dev-deps often include testing frameworks or benchmarks that use unsafe for performance. You care about unsafe in production dependencies, not in test helpers.

Geiger counts radiation, not cancer. High unsafe counts demand context, not panic. Read the code if the numbers worry you.

A realistic scan

Imagine a project that uses serde for serialization and a hypothetical crate called hyper-fast-parsing for low-level data processing. The Cargo.toml looks like this.

# Cargo.toml
[dependencies]
# A real crate, but imagine this version has a CVE
serde = "1.0.100"
# A hypothetical crate that does low-level parsing
hyper-fast-parsing = "0.5.0"

You run cargo audit. It finds a vulnerability in serde.

# cargo audit
warning: Vulnerability found in serde 1.0.100
id: RUSTSEC-2021-0001
severity: High
patched_versions: >= 1.0.101

You update serde to 1.0.101. The warning disappears. You run cargo geiger. It flags hyper-fast-parsing.

# cargo geiger
Crate                    Unsafe  Extern  Raw Ptrs
serde                    2       0       0
hyper-fast-parsing       45      2       12

serde has very little unsafe code. This is good. It means the crate relies on safe Rust abstractions. hyper-fast-parsing has a lot of unsafe code. It also has extern blocks and raw pointers. This suggests the crate is doing low-level work. You need to decide if you trust the authors. If hyper-fast-parsing is critical to your security, you should review the code or find an alternative. If it is just a utility for parsing logs, the risk might be acceptable.

The decision depends on your threat model. cargo geiger gives you the data to make that decision. It does not make the decision for you.

Pitfalls and false alarms

cargo audit can produce false positives. Sometimes an advisory is filed for a vulnerability that does not affect all use cases. Or the advisory is outdated and the crate has been fixed in a way the database does not reflect. Use --ignore to suppress these warnings. Document the reason for ignoring an advisory in your project notes.

cargo geiger can produce false alarms. A crate might use unsafe to implement a safe abstraction. The unsafe code might be correct and well-tested. The count does not tell you if the unsafe code is correct. It only tells you that unsafe code exists. Also, cargo geiger might flag extern blocks for FFI. If the FFI is to a well-known library like OpenSSL, the risk is different than FFI to an unknown C library.

Another pitfall is ignoring Cargo.lock. If you do not commit Cargo.lock to version control, cargo audit checks the resolved versions from Cargo.toml. This can differ from the actual versions installed. Always commit Cargo.lock for applications. For libraries, you can use --ignore-dev to skip dev-dependencies.

Convention aside: cargo audit supports --json output. Use this format for CI pipelines that need to parse the results. It makes it easier to integrate with security dashboards or reporting tools.

The compiler protects your memory, not your reputation. Audit your dependencies regularly.

Decision matrix

Use cargo audit when you need to check for known security vulnerabilities in your dependency tree. Run it in CI to block builds with unpatched CVEs. Use cargo audit when you are preparing a release and need to verify your Cargo.lock against the latest advisory database.

Use cargo geiger when you are evaluating a new dependency and want to gauge its trust surface. Run it to see if a crate relies heavily on unsafe blocks or FFI before you add it to a security-sensitive project. Use cargo geiger when you suspect a crate might be doing something sketchy and need a quick metric to decide if a manual code review is worth the time.

Reach for manual code review when cargo geiger flags a critical crate with high unsafe usage. Tools give you numbers; only reading the code tells you if those numbers are justified. Trust, but verify with cargo audit.

Where to go next