Application Security

Reachability Analysis for Rust and Cargo in 2026

How reachability analysis cuts noise for Rust services: cargo features, conditional compilation, RustSec advisories, and the tools that handle Rust well.

Daniel Chen
Staff Engineer
6 min read

Rust is the ecosystem where reachability analysis is most likely to actually work, and also the one where it can mislead you the fastest. The compiler's strong type information and the absence of dynamic dispatch in most code make static call graphs unusually precise. The catch is that Cargo features, conditional compilation, and procedural macros can shift entire modules in and out of the build, and a naive reachability tool that ignores feature resolution will produce results that look authoritative and are quietly wrong.

This post walks through what reachability analysis looks like on real Rust services, what the popular tools handle and what they don't, and how to read RustSec advisories with reachability in mind. The goal is to give a Rust team a defensible answer to the question security keeps asking, which is whether a given CVE actually touches their binary.

Why is Rust unusually well-suited to reachability?

Rust gives a reachability engine three things most other ecosystems do not. First, monomorphization means generic code is specialized at compile time, so the call graph is concrete rather than parametric. Second, the lack of runtime reflection and dynamic dispatch outside of explicit trait objects narrows the set of edges a static analyzer has to over-approximate. Third, the build artifact is a single binary with a known set of linked crates, so there is no ambiguity about which version of a dependency is actually present.

The practical consequence is that a Rust call graph built from MIR or LLVM IR is often within a few percent of the true runtime call graph for a given binary. That is far better than the over-approximations that plague JavaScript or Python reachability tools. It also means that when a reachability tool says a CVE is unreachable in a Rust binary, you can usually trust that answer in a way you cannot for most other ecosystems.

How do Cargo features and conditional compilation change the picture?

Cargo features are the single biggest source of confusion in Rust reachability reports. A vulnerable function in tokio's process module is unreachable if your crate does not enable the process feature, but a tool that resolves dependencies without performing feature unification will still list it. We have seen vendors flag CVE-2024-43806 in rustix as reachable on services that never enabled the termios feature, generating dozens of false-positive tickets per quarter.

Conditional compilation via cfg attributes adds another layer. Code gated behind cfg(unix) is genuinely absent from a Windows build, and code behind cfg(test) is absent from a release binary. A reachability tool that ignores cfg evaluation will overcount. The tools that do this well, Endor Labs and Snyk Code as of their 2026 Rust support, perform feature resolution against the actual Cargo.lock and target triple before walking the call graph. The ones that don't will give you a noisy report that erodes engineering trust within a quarter.

Which tools handle Rust reachability competently?

CodeQL added meaningful Rust support in late 2025, with MIR-based call graph extraction that handles monomorphization correctly. It is the strongest free option for Rust reachability today, though its query library is still thinner than its Java or JavaScript equivalents. Endor Labs ships a Rust-specific analyzer that consumes cargo build output and produces reachability annotations against RustSec advisories, with feature resolution baked in. Semgrep has pattern-based rules for Rust but does not build a true call graph, so its reachability claims are weaker than its claims for languages with first-class support.

The interesting open-source option is cargo-audit combined with cargo-geiger for unsafe-block surfacing, plus a custom call-graph layer built on rustc --emit=mir. This is what most large Rust shops we have seen end up running internally, because the off-the-shelf tools still miss workspace-level reachability that crosses crate boundaries within a single repository.

What does this look like for a real CVE?

Take RUSTSEC-2024-0421, the idna punycode parsing issue that affected a long tail of crates. The advisory rates it as medium severity with a CVSS of 6.3. On a typical Rust web service that pulls idna transitively through url and reqwest, the vulnerable code path is reachable only if the service parses externally-supplied IDN hostnames. A reachability analysis that walks from your HTTP request handlers down through reqwest::Client::get and into url::Url::parse will reach the vulnerable function, and the CVE is genuinely reachable.

Compare that to RUSTSEC-2023-0072, an issue in the openssl-src crate's bundled OpenSSL that only triggers when a specific deprecated function is called. Most services that link OpenSSL through openssl-sys never call that path. Reachability analysis correctly downgrades it to unreachable, and a team that trusts the tool can defer the upgrade without anxiety. The roughly 60% false-positive reduction we see on Rust SCA findings comes mostly from cases like this one.

How should you wire reachability into Rust CI?

The pragmatic pattern is to run reachability analysis on the release binary build, not on the dependency graph alone. Build with the production feature set, emit MIR or LLVM IR, and feed it to your reachability tool of choice. Gate merges on reachable-critical CVEs only, and surface unreachable findings as informational. This preserves engineering velocity while still catching the issues that matter.

A common mistake is to run reachability on a default cargo build rather than the production profile. Default builds enable debug assertions and often pull in dev-dependencies, which inflates the call graph and produces false positives. Always build with the actual production feature flags, and re-run reachability when you change them.

How Safeguard Helps

Safeguard ingests Rust SBOMs from cargo-cyclonedx or syft output and runs feature-aware reachability against RustSec and OSV advisories. Griffin AI explains, for each finding, which functions in your binary reach the vulnerable code and which feature flags or cfg conditions gate it. SBOM diffs flag when a Cargo.lock update introduces new reachable critical CVEs, and policy gates can block merges on reachable-only criteria so non-reachable advisories stay informational. TPRM tracks crate-level maintainer health and our zero-CVE container images give Rust services a clean base layer, so production binaries start from a known-good supply chain.

Never miss an update

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