Safeguard
Industry Analysis

Rust memory safety vulnerabilities despite the borrow che...

The borrow checker doesn't stop everything. Here's how rust unsafe code vulnerabilities slip past Rust's safety guarantees and reach production.

Aman Khan
AppSec Engineer
7 min read

A payments startup shipped a Rust rewrite of its ledger service in 2023, confident that "if it compiles, it's safe." Six months later, a fuzzing run turned up a heap buffer overflow inside a single unsafe block used to parse binary protocol frames — the kind of bug the borrow checker is famous for eliminating. It wasn't an isolated fluke. Rust's compiler guarantees hold only for safe code; the moment a developer writes unsafe { }, those guarantees are suspended, and the programmer inherits the same responsibilities a C developer has always carried. Understanding rust unsafe code vulnerabilities matters because nearly every non-trivial Rust program — including the standard library itself — contains unsafe code somewhere in its dependency tree, whether the team that shipped it realizes it or not.

Why do rust unsafe code vulnerabilities still happen if the borrow checker works?

They happen because the borrow checker's proofs stop at the unsafe keyword. Rust's memory safety model is built on a contract: the compiler statically verifies ownership, lifetimes, and aliasing rules for safe code, and in exchange the programmer promises that anything wrapped in unsafe upholds the same invariants the compiler can no longer check — no dangling pointers, no data races, no out-of-bounds access, no calling undefined-behavior-inducing FFI incorrectly. That promise is enforced by human review, not by the type system. When it's broken, the result isn't a "safe Rust bug" that just happens to feel unfamiliar — it's the same class of memory corruption you'd get from a bad memcpy in C, and it can undermine soundness in code that never itself writes the word unsafe. This is the core of rust borrow checker limitations: the tool only checks what it's designed to check, and raw pointer dereferences, manual memory layout tricks, and FFI boundaries were deliberately carved out of its scope so Rust could still do systems programming.

What actually goes wrong inside an unsafe block?

The most common failures are buffer overreads/overwrites from manual indexing, use-after-free from incorrect lifetime extension, double-frees in custom Drop implementations, and data races from unsafe impl Send/Sync on types that aren't actually thread-safe. A well-documented example is CVE-2018-1000810 in the smallvec crate, where insert_many used unsafe pointer arithmetic to shift elements and, under a specific panic condition, left the vector in a state that produced a double-free — a textbook C-style memory bug reintroduced through hand-rolled unsafe code in a crate downloaded millions of times. Another is CVE-2022-21658 (RUSTSEC-2022-0013), a time-of-check/time-of-use race in the Rust standard library's std::fs::remove_dir_all, fixed in Rust 1.58.1 in January 2022: an attacker could swap a directory for a symlink between the check and the delete, and no amount of borrow-checking prevented it because the flaw was in the logic sequencing, not in ownership. Neither bug involved a rookie mistake — both were written by experienced maintainers, which is the point. Unsafe rust demands the same discipline as C, without the decades of tooling (Valgrind, ASan-by-default CI, static analyzers baked into every editor) that C shops have built up around it. Teams that do try to police this manually often turn to tools like cargo-geiger, which counts unsafe usage per dependency, or Miri, an interpreter that can catch some undefined behavior in unsafe blocks during testing — but both are opt-in, neither runs by default in CI, and neither catches every unsound pattern, particularly ones that only manifest under specific allocator states or concurrent scheduling.

How common is unsafe code in the Rust ecosystem?

It's far more common than most consumers of a crate ever check. Empirical studies of crates.io have repeatedly found that a large share of popular crates use unsafe directly, and because Cargo pulls in transitive dependencies automatically, the practical number of projects that depend on unsafe code somewhere in their supply chain is close to universal — foundational crates like libc, bytes, hashbrown, mio, and much of the async runtime ecosystem rely on unsafe for performance, FFI, or data structures the type system can't express safely. The public RustSec Advisory Database, maintained by the Rust Secure Code working group, has accumulated hundreds of entries since it launched in 2017, and a meaningful fraction are tagged "memory-unsafety" or "soundness" rather than logic bugs — meaning the fix wasn't "add a null check," it was "this unsafe block violated Rust's memory model." Every one of those advisories represents a moment where "written in Rust" did not mean immune to memory corruption.

Where do rust borrow checker limitations show up in production incidents?

They show up most often at FFI boundaries and in performance-critical hot paths, not in everyday application logic. The actix-web project's 2020 crisis is the canonical case study: a security researcher audited the popular web framework and found more than a dozen unsound uses of unsafe — including transmutes and manual lifetime manipulation — that could be triggered from safe-looking application code, meaning developers using actix-web with zero unsafe blocks of their own could still hit undefined behavior. The framework's original maintainer initially dismissed the findings, the report went public in February 2020, and the fallout led to a new maintainer team rewriting the unsafe internals and adding #![forbid(unsafe_code)] gates to safer subcomponents. The lesson generalized well beyond one framework: rust memory safety only extends as far as the unsafe code your dependencies chose to write, and you frequently can't see that code without reading source, because cargo tree shows you dependency names, not risk.

Can memory safety issues in unsafe code be exploited like classic C vulnerabilities?

Yes, and in some cases identically. Buffer overflows in unsafe Rust are exploitable the same way as in C — a crafted input that overruns a bounds check bypassed by get_unchecked or a raw pointer offset can corrupt adjacent heap memory, and depending on allocator layout and mitigations present, that can be leveraged for information disclosure or, in weaker cases, control-flow hijacking. Rust doesn't add hardening beyond what the compiler and OS already provide (stack canaries, ASLR, and similar) once you're inside unsafe territory — you've opted out of the language's additional protection. This is why security teams that treat "written in Rust" as a checkbox for a vendor questionnaire are missing the actual risk surface: the question isn't language choice, it's how much unsafe code exists, where it lives, and whether it's been audited. That distinction matters for compliance too: SOC 2 and secure-SDLC frameworks increasingly ask organizations to demonstrate they understand their dependency risk, and "we use Rust" is not an answer an auditor should accept without evidence of what's actually inside that dependency tree.

How Safeguard Helps

Safeguard treats "the codebase is written in Rust" as a starting point, not a conclusion. Our software supply chain security platform scans dependency trees to identify exactly which crates introduce unsafe blocks, cross-references them against the RustSec Advisory Database and CVE feeds for known memory-safety and soundness issues, and flags newly introduced unsafe code in pull requests before it merges — so a get_unchecked call or a raw pointer cast doesn't sit unreviewed in a hot path for six months. We also track transitive unsafe exposure, so teams can see when a dependency they trust pulls in a sub-dependency with unaudited unsafe internals, and we surface that risk continuously as the dependency graph changes rather than as a one-time audit. For teams shipping Rust in production, that means rust unsafe code vulnerabilities get caught at the same stage as any other supply chain risk: before they ship, not after a fuzzer or an attacker finds them first.

Never miss an update

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