Safeguard
Open Source Security

Unsafe Rust code vulnerability patterns

RustSec advisories tied to unsafe code keep climbing. Here's how unsound FFI, transmute misuse, and unchecked indexing become real exploits.

Safeguard Research Team
Research
7 min read

SAN FRANCISCO — July 6, 2026. Rust entered 2026 as the fastest-growing systems language in production infrastructure, embedded in everything from Linux kernel modules to cloud control planes. Yet the same six-year stretch that cemented Rust's reputation as the "memory-safe" alternative to C and C++ has also produced a steady drumbeat of advisories in the RustSec database tied to a single keyword: unsafe. As of this year, RustSec has logged well over 700 advisories against crates.io packages, and a recurring share of the most severe ones — those rated critical or high under RUSTSEC scoring — trace back to unsafe blocks that quietly broke the compiler's memory-safety guarantees. For security teams that adopted Rust specifically to eliminate memory-corruption bugs, that is an uncomfortable statistic, and it is why "unsafe rust code vulnerability" has become one of the fastest-rising search terms among application security researchers this year.

The pattern is not a Rust failure so much as a Rust feature being misused at scale. unsafe is a deliberate escape hatch — it lets developers write raw-pointer arithmetic, call into C libraries, and bypass the borrow checker when performance or FFI interoperability demands it. Academic sampling of crates.io has repeatedly found that somewhere between a quarter and a third of published crates contain at least one unsafe block, and a much larger share depend transitively on a crate that does. Every one of those blocks is a place where Rust's foundational promise — no use-after-free, no data races, no buffer overflows — is suspended, and where all the classic C/C++ vulnerability classes can, and do, reappear.

Why "Memory Safe" Doesn't Mean "Unsafe-Block Safe"

The core issue security teams need to internalize is that Rust's safety guarantees are a sound type system property of safe code, not a property that automatically extends across an unsafe boundary. Once a developer writes unsafe { ... }, the compiler stops checking aliasing, lifetime, and initialization invariants inside that block and instead trusts the developer to uphold them manually. If the developer gets it wrong — and empirical studies of real-world unsafe code consistently show a meaningful fraction do — the bug doesn't stay contained. It can silently poison code far outside the unsafe block itself, because safe Rust code calling into a broken unsafe abstraction inherits whatever invariant violation that abstraction introduced. A single unsound unsafe fn buried three dependencies deep in a supply chain can turn an entire "safe" call graph into exploitable memory corruption.

The Recurring Vulnerability Patterns

Analysis of RustSec advisories and public CVE data converges on a small number of repeat offenders:

  • Unsound FFI boundaries. The most common source of real-world incidents. Rust code marshaling data to and from C libraries via extern "C" frequently mishandles buffer lengths, string termination, or ownership transfer, producing classic buffer overflows and use-after-free bugs at the exact seam where Rust's guarantees end and C's don't begin.
  • transmute and type punning. Reinterpreting a byte sequence as a different type without validating size, alignment, or bit-pattern validity is a frequent root cause of undefined behavior, especially when transmuting between types with different validity invariants (e.g., arbitrary bytes into bool or char).
  • Unchecked indexing via get_unchecked/get_unchecked_mut. Used for performance in hot loops, these APIs skip bounds checks entirely. A single off-by-one in index computation becomes a full out-of-bounds read or write with no panic to catch it — this was the exact shape of the 2021 smallvec insertion flaw (RUSTSEC-2021-0003), where a capacity-calculation error let callers write past the end of a heap-allocated buffer.
  • Improper MaybeUninit handling. Working with uninitialized memory for performance reasons is legitimate, but reading from a MaybeUninit<T> before it is fully initialized, or assuming zeroed() is valid for types that forbid certain bit patterns, produces undefined behavior that sanitizers frequently fail to catch in CI.
  • Unsound Send/Sync implementations. Manually asserting that a type is safe to share or move across threads — often done to work around the borrow checker for a "known safe" scenario — has repeatedly proven wrong in practice, reintroducing data races into concurrent Rust code that was supposed to be race-free by construction.
  • Raw pointer aliasing and double-free in custom Drop. Hand-rolled smart pointers and arena allocators that manage their own deallocation logic are a recurring source of double-free and use-after-free bugs when error paths or panics interrupt the expected drop sequence.
  • Platform-level environment race conditions. The time crate's 2020 advisory (RUSTSEC-2020-0071, CVE-2020-26235) showed that even indirect misuse of unsafe — calling localtime_r in a multithreaded process without holding a documented environment lock — could cause memory corruption and segfaults triggered by an entirely different crate calling set_var elsewhere in the process. That single crate was a transitive dependency of an enormous share of the ecosystem, illustrating how one unsafe misstep propagates through the dependency graph.

Why This Is a Supply Chain Problem, Not Just a Coding Problem

What makes these patterns a software supply chain concern rather than a purely developmental one is depth and blast radius. The smallvec and time incidents didn't just affect the crate authors — they affected every downstream project that pulled the vulnerable version transitively, often without any awareness that an unsafe block existed in their dependency tree at all. A team writing entirely safe Rust in their own codebase can still ship a memory-corruption vulnerability to production because of a single unsound function nested four or five levels down in Cargo.lock. Traditional SCA tooling that only checks whether a known-CVE version string is present in a manifest catches the advisory after the fact; it says nothing about whether the vulnerable unsafe code path is actually reachable from the application's own execution paths, which is the question that determines real exploitability and remediation priority.

Compounding the problem, Rust's compiler cannot statically prove the absence of undefined behavior inside unsafe blocks, so traditional fuzzing and sanitizer coverage (Miri, AddressSanitizer, cargo-fuzz) — while valuable — only catch what test suites actually exercise. Unsafe code paths that only trigger under specific input sizes, thread interleavings, or allocator behavior can sit dormant in production dependencies for years, exactly as the time crate flaw did before disclosure.

What Security Teams Should Be Doing Now

Given this trend, mature application security programs are shifting from "does a CVE exist for this crate version" to a three-part question: does the vulnerable unsafe code exist in the dependency, is that code path reachable from the application's actual call graph, and if so, is remediation available or does it require a compensating control. Static presence of unsafe in a dependency tree is not itself an emergency — the vast majority of unsafe code in the ecosystem is correct and load-bearing for performance — but it is precisely the signal that should trigger deeper reachability and provenance analysis rather than being ignored or, conversely, treated as uniformly critical.

How Safeguard Helps

Safeguard is built for exactly this gap between advisory noise and exploitable reality. Our reachability analysis traces whether a vulnerable unsafe function — like an unsound FFI wrapper or an unchecked-indexing helper flagged in a RustSec advisory — is actually invoked along a path reachable from your application's entry points, so teams can triage the handful of dependencies that matter instead of the hundreds that merely appear in Cargo.lock. Griffin, Safeguard's AI-driven vulnerability analysis engine, reads the unsafe block itself, models the invariant it's supposed to uphold, and explains in plain language why a given transmute, pointer cast, or Send/Sync assertion is exploitable in your specific call context, cutting triage time from days to minutes. Safeguard's SBOM generation and ingestion pipeline gives security teams continuous, accurate visibility into every crate — including transitive dependencies several layers deep — that introduces unsafe code into a Rust build, closing the blind spot that let incidents like the time crate advisory catch so much of the ecosystem by surprise. And where a fix is available upstream, Safeguard's auto-fix PRs open the dependency bump directly against the affected repository, with reachability context attached, so remediation ships as fast as the advisory does.

Never miss an update

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