Rust Standard Library Memory Safety Bugs
A Rust programmer can write code with no unsafe block and still hit undefined behavior. Microsoft’s FORGE Lab documented exactly that on November 9, 2026, in a post on the Microsoft Security Blog. The bugs live in Rust’s core and std libraries. The caller did nothing wrong.
That is the whole trick of a soundness bug, and it is why the FORGE Lab work matters more than a single patch. A safe API is only sound if the unsafe code beneath it holds for every caller. Vec and String are built from small unsafe pieces, so the guarantee carries the entire language. The tooling that answers the question for the standard library is still being assembled.
Here is the shape of it. A safe function checks a length, then reads through a second call that can report a different length. The compiler type-checks both calls without complaint.
Key Takeaways:
- A soundness bug means a caller using only safe code can reach undefined behavior. The defect belongs to the library, never to the caller who tripped it.
- Miri validates aliasing for pure Rust but cannot see inside foreign functions. Paired with an LLVM interpreter, a study found 46 instances of undefined or undesired behavior across 37 libraries.
- RULF generated fuzz targets by traversing API graphs: 7 to 118 targets per library, API coverage up to 0.92, 30 previously unknown bugs across seven libraries.
- deepSURF covered 63 crates, rediscovered 30 known memory safety bugs, and found 12 new ones; 11 received RustSec IDs and 3 were patched.
- An empirical study of 4,600 GitHub projects found 81.5% kept outdated dependencies, and 69% of surveyed developers did not know they had vulnerable ones.
- No published figure states how many unsafe blocks
stdcontains, or what share has been verified.
What a sound safe API promises
A safe API is sound when no safe caller, however it uses that API, can trigger undefined behavior. That is the promise the Rustonomicon’s safe-unsafe contract attaches to every unsafe block, and it extends upward to the public function wrapped around it. Vec and String are built from small unsafe pieces, so the guarantee carries the whole language.

The failure mode is hard to spot because the broken assumption usually sits between two calls that look identical. This safe function checks a length, then reads through a second call that can report a different length:
use std::cell::Cell;
fn read_at<S: AsRef<[u8]>>(buf: &S, idx: usize) -> Option<u8> {
if idx < buf.as_ref().len() {
// SAFETY: the branch above checked idx against a length.
// It checked a *different call's* return value.
Some(unsafe { *buf.as_ref().get_unchecked(idx) })
} else {
None
}
}
struct ShrinkingView {
calls: Cell<u32>,
data: Vec<u8>,
}
impl AsRef<[u8]> for ShrinkingView {
fn as_ref(&self) -> &[u8] {
let n = self.calls.get();
self.calls.set(n + 1);
if n == 0 { &self.data } else { &[] }
}
}
fn main() {
let view = ShrinkingView { calls: Cell::new(0), data: vec![10, 20, 30, 40] };
// First call reports len 4, the second returns an empty slice.
println!("{:?}", read_at(&view, 3));
}
// Compiles without a warning and reads past the end of an empty slice.
// Note: the caller contains no unsafe block; the library does.
// Run it under Miri to get a diagnostic instead of a garbage byte.
S is picked by the caller, and a safe AsRef implementation is free to return a long slice the first time and an empty one the second. Unsafe code has to treat every call it makes as untrusted. Easy to say. Easy to forget when the call is a trait method the compiler type-checks without complaint. Multiply that by every public function in core and std and the size of the review problem becomes clear.
The fix is smaller than the bug. Bind the reference once and index it safely; the bounds check that remains is the same one the fast path tried to skip, so nothing is lost in speed:
fn read_at<S: AsRef<[u8]>>(buf: &S, idx: usize) -> Option<u8> {
let bytes = buf.as_ref(); // one call, one length, one slice
bytes.get(idx).copied()
}
// No unsafe block and no invariant to maintain. Note: this fixes the wrapper
// only. The deeper lesson is that a safe API should not let a caller-supplied
// impl answer the same question twice.
Three shapes a soundness bug takes
Across the published work, the failures cluster into three repeatable shapes: the guard and the access disagree about the state of the data, a partially initialized value is dropped during unwinding, and the unsafe code sits at a boundary no dynamic checker can cross. The second shape is the one most likely to appear in ordinary container code.

use std::mem::MaybeUninit;
const N: usize = 4;
struct Window<T> { slots: [MaybeUninit<T>; N], len: usize }
impl<T: Clone> Clone for Window<T> {
fn clone(&self) -> Self {
// len is set first, so Drop already trusts N initialized slots.
let mut out = Window { slots: [const { MaybeUninit::uninit() }; N], len: self.len };
for i in 0..self.len {
let v = unsafe { self.slots.get_unchecked(i).assume_init_ref() }.clone();
unsafe { out.slots.get_unchecked_mut(i) }.write(v);
}
out
}
}
impl<T> Drop for Window<T> {
fn drop(&mut self) {
for i in 0..self.len {
// SAFETY: assumes every slot below len is initialized.
unsafe { self.slots.get_unchecked_mut(i).assume_init_drop() };
}
}
}
// If T::clone panics at element 2, unwinding drops `out` with len == 4 but
// only two live slots, so Drop reads uninitialized memory.
// Note: reduced illustration. Production code would stage the new buffer in
// ManuallyDrop and publish len only after every element is written.
Nothing in the caller looks dangerous: a clone that panics is ordinary, and unwinding is ordinary. The invariant that breaks is internal to the container, which is exactly why this class of bug survives code review. Staging the partially built value in ManuallyDrop and setting the length last turns a use-after-free into a leak, and a leak during a panic is allowed.
How a finding becomes a RustSec advisory
A bug report and an advisory answer different questions. A report tells maintainers a hole exists and who is fixing it. An advisory names affected versions so a dependency scanner can act without a human in the loop. deepSURF shows where the two meet: of the 12 previously unknown vulnerabilities it found, 11 were assigned RustSec IDs and three were patched at the time of writing, according to the deepSURF paper.
Crate-level findings fit that pipeline well because a consumer can bump a version. A bug inside core or std does not fit as neatly: the fix ships with the toolchain, so the practical question becomes which release carries the patch rather than which dependency to swap. That is one reason the standard library needs continuous verification instead of periodic audits.
Advisories also only work if teams apply them. An empirical study covering more than 4,600 GitHub projects and 2,700 library dependencies found that 81.5% of the studied systems still kept outdated dependencies, and 69% of interviewed developers claimed they were unaware of their vulnerable ones, per the 2017 dependency-migration study. Better discovery raises the value of that maintenance work rather than replacing it.
Miri checks aliasing, and stops at the FFI boundary
The authors ran Miri and an LLVM interpreter together so foreign calls could execute while the surrounding Rust code stayed checked, then applied that setup to real libraries. They found 46 instances of undefined or undesired behavior across 37 libraries. Three of the bugs were in libraries averaging more than 10,000 daily downloads during the observation period, and one was in a library maintained by the Rust Project itself.
Many of those 46 were violations of the aliasing models, and the newer Tree Borrows model turned out to be significantly more permissive than the earlier Stacked Borrows model. That detail changes how the result should be read: which findings count as bugs depends on which model is being enforced at the time.
Library fuzzing needed harnesses; generated ones cut the cost
Fuzzing a library is not the same as fuzzing a binary. There is no input file to mutate, only a public API whose calls must be sequenced and whose arguments must satisfy type invariants. RULF (arXiv:2104.12064v3) names the consequence plainly: existing fuzzing tools are not applicable to library APIs because fuzz targets do not exist, and writing them by hand is labor intensive, one case at a time. RULF generated targets by traversing API dependency graphs, produced between 7 and 118 targets per library with API coverage up to 0.92, and found 30 previously unknown bugs across seven libraries.
deepSURF attacks the same bottleneck from the other end, combining static analysis with LLM-guided harness generation and aiming specifically at unsafe code. Its answer to generics is the interesting part: instead of giving up on trait-bounded parameters, it substitutes custom types and writes tailored implementations of the required traits so the fuzzer can behave like a user-defined type inside the library. LLMs then extend harnesses dynamically to reach deeper API call sequences.
On 63 real-world crates it rediscovered 30 known memory safety bugs and uncovered 12 previously unknown vulnerabilities. Those numbers are strong for one tool on one benchmark, and they are also a measurement of 63 crates, which is why the standard library needed a different approach.
Five discovery and verification efforts compared
Each effort below checks a different surface with a different method, so the counts are not additive and should not be summed into a single “state of Rust safety” figure.
| Effort | Surface checked | What it reported | Source |
|---|---|---|---|
| Miri plus an LLVM interpreter | 37 Rust libraries that call foreign functions | 46 instances of undefined or undesired behavior; 3 in libraries averaging over 10,000 daily downloads | arXiv:2404.11671v8 |
| deepSURF (static analysis plus LLM harnesses) | 63 real-world crates, unsafe code focus | 30 known bugs rediscovered, 12 new, 11 with RustSec IDs, 3 patched | arXiv:2506.15648v2 |
| Community verification of the standard library | Unsafe code in the Rust standard library | At least 50 contributors; remaining obstacles framed as open challenges for formal methods | arXiv:2510.01072v3 |
| CI-integrated verification campaign | A repository forked from the Rust standard library | Described by its authors as the largest verification campaign reported for a software library | arXiv:2606.17374v1 |
The last two rows address the standard library directly. The community effort report says the work was convened by Amazon, runs in public, and is open source, and it is candid that parts of the problem remain unsolved. The CI campaign paper describes wiring complementary verification tools into continuous integration so machine-checked proofs cover a subset of undefined behaviors: out-of-bounds access, null and dangling pointer dereferences, and use of uninitialized memory. Neither claims the library is verified.
The tool evaluations themselves get graded now
Once fuzzers become the measurement instrument, the instrument needs checking. A 2026 methodology paper (arXiv:2610.20315v1) notes that empirical studies have found hundreds of memory safety issues in Rust programs, including the standard library, and that published evaluations of security tools have contained serious issues that undermined reproducibility. Its answer is an automated way to inject unsafe code coverage oracles into test sets plus a method for quantifying how diverse a Rust test set is, applied to popular Rust fuzzers across a test set of 20 Rust projects.
Keep that in mind when reading any bug count in this article. A tool measured on crates it was tuned for will look stronger than the same tool on a diverse corpus, and a harness that never reaches an unsafe block cannot find the bug inside it.
What safe Rust libraries are betting on
The safety promise is being tested hardest by libraries that cannot trade speed for it. kornia-rs (arXiv:2505.12425v1) reports a 3 to 5 times speedup in image transformation tasks over native Rust alternatives while staying comparable to C++ wrapper-based libraries, and ships Python bindings for cross-language use. ad-trait (arXiv:2504.15976v1) describes itself as the first operator-overloading automatic differentiation implementation in Rust to offer both forward and reverse mode, with SIMD acceleration in forward mode, and reports being among the fastest AD implementations across several languages on its benchmarks.
Two more fall into the same group: RustSAT (arXiv:2505.15221v1) wraps several leading SAT solvers behind one Rust API and adds encodings for at-most-one, cardinality, and pseudo-Boolean constraints, and RISE (arXiv:2606.07187v1) reports inverted-index query speedups of up to 2x over the current state of the art. Every one of those numbers comes from the authors’ own benchmark sets, which is the normal arrangement and also the reason a soundness bug in a core primitive propagates into code nobody benchmarks for it.
Kernel adoption raises the stakes on std soundness
These bugs get attention because of what Rust does outside its own standard library. A TechTimes piece marking Linux’s 35th anniversary reports that Rust is now a permanent part of the kernel, following a period in which memory bugs drove roughly 80 percent of CVEs, according to the August 2026 report. Kernel code does not use std, but it uses the same language guarantees and the same core types, so a hole in core is a hole in the argument that brought Rust into the kernel.

What these counts do not measure
Every figure above is scoped to the tool and sample that produced it. 46 instances across 37 libraries is a library sample, 63 crates is a crates.io sample, and seven libraries is smaller still. None of them is a census of the standard library, and none establishes a rate of unsound APIs per thousand lines of unsafe code.
That denominator has not been published. Nobody has stated how many unsafe blocks std contains or what share has been formally verified, which leaves “how unsafe is the standard library” open even with the November 2026 findings in hand. Both verification efforts describe their own coverage as partial.
Finding a bug is not the same as a regression. A better agentic or LLM-assisted pipeline finds more of the same bugs, and more findings usually mean the search improved rather than the library decayed. The papers support that reading: RULF and deepSURF both describe reaching surfaces that hand-written harnesses had not covered, and the 2026 methodology work exists precisely because an unevaluated harness can look impressive while missing the unsafe block it was built to reach.
For a developer writing safe Rust, none of this changes what you type. It changes what you should assume about the layer underneath: safe means the compiler checked your side of the contract, and the other side is maintained by a project still building the tooling to prove it. Microsoft’s wider Rust commitments are covered in our look at Microsoft’s Rust adoption, and the guarantees the compiler does enforce are walked through in the Rust compiler pipeline and borrow checker.
Related Reading
More in-depth coverage from this blog on closely related topics:
- SpaceX Starship Flight 15 Catch
- FAA Ground Stop: Nationwide Departure Halt
- ChatGPT Wallet: Agentic Checkout Explained
- Big Tech AI Capex: Who Can Fund It
- Microsoft Outlook Mailbox Risk Explained
Sources and References
Sources cited while researching and writing this article:
- FORGE Lab uses agentic tools to identify memory safety issues in Rust
- How Safe and Unsafe Interact – The Rustonomicon
- [2506.15648v2] deepSURF: Detecting Memory Safety Vulnerabilities in Rust Through Fuzzing LLM-Augmented Harnesses
- [1709.04621v1] Do Developers Update Their Library Dependencies? An Empirical Study on the Impact of Security Advisories on Library Migration
- [2104.12064v3] RULF: Rust Library Fuzzing via API Dependency Graph Traversal
- arXiv:2606.17374v1
- arXiv:2610.20315v1
- arXiv:2505.12425v1
- arXiv:2504.15976v1
- arXiv:2505.15221v1
- arXiv:2606.07187v1
- Linux at 35: Rust Now Permanent in Kernel After Memory Bugs Drove 80 Percent of CVEs
Rafael
Born with the collective knowledge of the internet and the writing style of nobody in particular. Still learning what "touching grass" means. I am Just Rafael...
