Skip to content

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.

Published on 6 min read

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 classC / C++Garbage-collected (Go, Java, C#, Swift*)Safe Rust
Buffer overflowPossiblePrevented by bounds checks (runtime panic/exception)Prevented by bounds checks (panic)
Use-after-freePossiblePrevented by GCPrevented by ownership at compile time
Double freePossiblePrevented by GCPrevented by ownership
Uninitialised readsPossiblePrevented (zero values / definite assignment)Prevented at compile time
Data racesPossiblePossible in Go and Java (not memory-unsafe in Java)Prevented at compile time (Send/Sync)
Integer overflowUB (signed) or wrapWraps or checked, depending on languagePanics in debug, wraps in release unless checked
Logic bugs, injection, auth errorsPossiblePossiblePossible

*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 Vec out of range panics instead of reading adjacent memory; get() returns an Option for code that wants to handle it.

Where the guarantees stop

  • unsafe blocks let code dereference raw pointers, call foreign functions and implement low-level abstractions. The standard library itself relies on them. Keep unsafe small, 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, cxx and 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 unsafe code. cargo audit checks for known advisories, and tools such as cargo-geiger report where unsafe is 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.

PriorityWhat to move or write in a memory-safe languageWhy
1New components and new featuresVulnerabilities concentrate in new and recently changed code
2Parsers, decoders, codecs, protocol handlersThey process untrusted input and are historically bug-dense
3Network-facing services and sandboxes' outer layersHighest attack surface
4Components with a history of memory-safety CVEsEvidence of risk
5Stable, well-fuzzed internal codeLowest return on rewrite; harden instead

Practical steps:

  1. 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.
  2. 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.
  3. Replace, don't translate. Rewriting a parser in Rust is an opportunity to redesign its API around ownership, not to transliterate pointer arithmetic.
  4. 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_ASSERTIONS for libstdc++, or libc++'s hardening modes (-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST in recent LLVM releases). These make operator[], front(), iterator misuse and similar errors trap instead of corrupting memory, at low cost.
  • Views instead of pointer-and-length. std::span and std::string_view carry their size with them.
  • Ownership in types. std::unique_ptr and std::shared_ptr instead of owning raw pointers; no new/delete in application code.
  • Compiler help. Clang's -Wunsafe-buffer-usage flags 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.
  • unsafe code 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.

Related guides

0x1000 · Vulnerability Classes

Stack Buffer Overflows Explained for Defenders

Why writing past a stack buffer is dangerous, the C patterns that cause it, how compilers and sanitizers catch it, and the fixes and mitigations that contain it.