Memory-Safe Languages and a Realistic Migration Strategy
Why memory-safe languages remove whole bug classes, what Rust's ownership model guarantees, where unsafe code and FFI remain risky, and how to harden the C++ you keep.
Every guide on this site so far has been about managing the consequences of memory-unsafe code: finding the bugs with sanitizers and fuzzing, and blunting them with mitigations. Memory-safe languages attack the problem at the source: they make buffer overflows, use-after-free and double free impossible to express in ordinary code. This guide explains what that guarantee covers, where it stops, and how teams move toward it without an all-or-nothing rewrite.
Why this became a policy topic
Several large vendors have published analyses of their security bugs. Microsoft and the Chromium project have both stated publicly that roughly 70% of the serious security bugs they fix are memory-safety issues. Google's Android team has reported that memory-safety vulnerabilities fell substantially as the share of new native code written in memory-safe languages (Rust, plus Java and Kotlin) grew, even though the existing C and C++ code was not rewritten.
Governments followed. The NSA published an information sheet on software memory safety in 2022 recommending memory-safe languages where possible, and CISA, together with partner agencies, published The Case for Memory Safe Roadmaps in 2023, asking software manufacturers to publish plans for reducing memory-safety vulnerabilities in their products. The message is consistent: memory safety is a product-level decision, not only a developer habit.
What "memory-safe" guarantees
| Bug class | C / C++ | Garbage-collected (Go, Java, C#, Swift*) | Safe Rust |
|---|---|---|---|
| Buffer overflow | Possible | Prevented by bounds checks (runtime panic/exception) | Prevented by bounds checks (panic) |
| Use-after-free | Possible | Prevented by GC | Prevented by ownership at compile time |
| Double free | Possible | Prevented by GC | Prevented by ownership |
| Uninitialised reads | Possible | Prevented (zero values / definite assignment) | Prevented at compile time |
| Data races | Possible | Possible in Go and Java (not memory-unsafe in Java) | Prevented at compile time (Send/Sync) |
| Integer overflow | UB (signed) or wrap | Wraps or checked, depending on language | Panics in debug, wraps in release unless checked |
| Logic bugs, injection, auth errors | Possible | Possible | Possible |
*Swift uses automatic reference counting rather than a tracing GC, with bounds-checked collections.
The key point for defenders: in the memory-safe column, an out-of-bounds access becomes a controlled panic or exception instead of silent corruption. A crash is still a bug and may still be a denial of service, but it is not an arbitrary memory write.
How Rust enforces it
Rust's compiler tracks who owns each value and how long references to it live:
fn longest_name(names: &[String]) -> Option<&str> {
names.iter().map(|s| s.as_str()).max_by_key(|s| s.len())
}
fn main() {
let result;
{
let names = vec![String::from("ada"), String::from("grace")];
result = longest_name(&names);
} // `names` is dropped here
println!("{result:?}"); // compile error: `names` does not live long enough
}
The same use-after-free that would compile silently in C is rejected before the program ever runs. Three rules do most of the work:
- Ownership. Each value has exactly one owner; when the owner goes out of scope, the value is dropped once. Double free cannot be expressed.
- Borrowing. You may have many shared references (
&T) or one mutable reference (&mut T), never both, and references cannot outlive the value. Iterator invalidation and dangling pointers are rejected. - Bounds checks. Indexing a slice or
Vecout of range panics instead of reading adjacent memory;get()returns anOptionfor code that wants to handle it.
Where the guarantees stop
unsafeblocks let code dereference raw pointers, call foreign functions and implement low-level abstractions. The standard library itself relies on them. Keepunsafesmall, wrap it in safe APIs, document the invariants with// SAFETY:comments, and run such code under Miri and sanitizers in tests.- FFI boundaries with C and C++ are where most memory-safety bugs in mixed code bases now concentrate: mismatched ownership, lifetimes the Rust side cannot see, and C code that was never safe to begin with. Tools such as
bindgen,cxxand careful ownership conventions help. - Panics are safe but can still take a service down. Handle untrusted input with fallible APIs rather than
unwrap(). - Dependencies. A crate can contain
unsafecode.cargo auditchecks for known advisories, and tools such ascargo-geigerreport whereunsafeis used.
A realistic migration strategy
The organisations that report progress share a common pattern: they changed what new code is written in, and prioritised by exposure.
| Priority | What to move or write in a memory-safe language | Why |
|---|---|---|
| 1 | New components and new features | Vulnerabilities concentrate in new and recently changed code |
| 2 | Parsers, decoders, codecs, protocol handlers | They process untrusted input and are historically bug-dense |
| 3 | Network-facing services and sandboxes' outer layers | Highest attack surface |
| 4 | Components with a history of memory-safety CVEs | Evidence of risk |
| 5 | Stable, well-fuzzed internal code | Lowest return on rewrite; harden instead |
Practical steps:
- Publish an internal roadmap, as CISA's guidance recommends: which languages are approved for new code, which components are migration candidates, and how progress is measured.
- Make interop boring. Establish a supported way to call memory-safe code from the existing C or C++ build (and vice versa) before the first component moves.
- Replace, don't translate. Rewriting a parser in Rust is an opportunity to redesign its API around ownership, not to transliterate pointer arithmetic.
- Keep fuzzing both sides. Fuzz the new implementation and, during transition, compare its output with the old one (differential fuzzing).
Hardening the C++ you keep
Most code bases will contain C and C++ for many years. Modern C++ offers much safer defaults than its reputation suggests, if you use them:
- Bounds-checked access. Enable standard library hardening:
-D_GLIBCXX_ASSERTIONSfor libstdc++, or libc++'s hardening modes (-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FASTin recent LLVM releases). These makeoperator[],front(), iterator misuse and similar errors trap instead of corrupting memory, at low cost. - Views instead of pointer-and-length.
std::spanandstd::string_viewcarry their size with them. - Ownership in types.
std::unique_ptrandstd::shared_ptrinstead of owning raw pointers; nonew/deletein application code. - Compiler help. Clang's
-Wunsafe-buffer-usageflags raw pointer arithmetic and array indexing so you can migrate code toward bounds-checked containers file by file. - Guidelines and checkers. The C++ Core Guidelines with clang-tidy's
cppcoreguidelines-*checks catch many ownership and bounds mistakes automatically. - The usual layers. Hardening flags, control-flow integrity, sanitizers in CI and continuous fuzzing.
Key takeaways
- Memory-safe languages turn buffer overflows, use-after-free and double free from exploitable corruption into compile errors or controlled failures.
- Industry data points in the same direction: most serious bugs in large C and C++ code bases are memory-safety bugs, and shifting new code to memory-safe languages reduces them without rewriting everything.
unsafecode and FFI boundaries are where the remaining risk concentrates; keep them small, reviewed and tested.- Prioritise new code and untrusted-input handling, publish a roadmap, and harden the C++ you keep with library assertions,
std::span, smart pointers and compiler checks.