1. Introduction
C and C++ give the programmer direct control over memory and ask the programmer, in return, to get every pointer dereference and every deallocation right. The languages do not check. A write past the end of an array or a read through a pointer to freed storage is undefined behaviour, and attackers have turned such errors into code execution since the earliest network worms. The systematisation by Szekeres et al. (2013) describes the result as a long contest in which each new defence has been met by a new attack technique, while the underlying errors continued to be written.
What has changed in recent years is the position of institutions. The United States National Security Agency advised organisations to prefer memory-safe languages where possible (NSA, 2022). A group of national cybersecurity agencies followed with a call for software manufacturers to publish roadmaps for eliminating this class of vulnerability (CISA et al., 2023), and the White House Office of the National Cyber Director made the same argument to a technical audience (ONCD, 2024). These documents rest on reports from large vendors that memory safety errors account for a large majority of their severe vulnerabilities.
The advice is simple to state and hard to follow, because the operating systems, browsers, databases, network stacks and embedded firmware concerned represent decades of work. This review is about the middle ground between doing nothing and rewriting everything: the strategies by which a legacy codebase can be moved, in whole or in part, towards memory safety.
2. Scope and Approach
This is a narrative review of the published literature. It draws on peer-reviewed papers from the programming languages, security and software engineering communities, on the principal government guidance, and on standard books. Sources were chosen for their influence on the field and for the range of strategies they represent, and no claim is made to exhaustive coverage. The review is confined to migration from C and C++, considers Rust as the main target language because the research literature does, and leaves aside garbage-collected languages, which suit application code better than the systems code at issue here.
Two terms recur. Spatial safety means that every access through a pointer stays within the object the pointer was derived from. Temporal safety means that no access occurs after the object has been freed. A strategy may provide one, both, or only a reduction in the exploitability of violations. Table 1 sets out the strategies used to organise what follows, with the rewrite family divided into its manual and automated routes.
| Family | Mechanism | Main limitation | Representative sources |
|---|---|---|---|
| Harden in place | Compiler instrumentation and runtime checks on unchanged source | Detects or impedes errors; does not remove them | Serebryany et al., 2012; Abadi et al., 2005 |
| Safe C dialects | Checked pointer types and annotations added to C | Annotation effort; safety only in converted regions | Necula et al., 2002; Elliott et al., 2018 |
| Capability hardware | Pointers carry bounds and permissions enforced by the processor | Needs new hardware; temporal safety is added separately | Woodruff et al., 2014 |
| Incremental rewrite | Components replaced by Rust behind a C interface | Boundary code is unsafe; skills and build integration | Anderson et al., 2016; Jung et al., 2021 |
| Automated translation | Tools convert C to Rust, then lift raw pointers to safe types | Output is largely unsafe Rust at first | Emre et al., 2021 |
3. Hardening Code in Place
The cheapest strategy leaves the source alone and changes how it is compiled and run. Sanitizers instrument the program to detect violations as they happen. AddressSanitizer surrounds objects with poisoned red zones, tracks the state of memory in a shadow map and delays the reuse of freed blocks, which lets it report overflows and many use-after-free errors with a slowdown that is tolerable in testing (Serebryany et al., 2012). Its guarantee is probabilistic and its cost is generally too high for production. The survey by Song et al. (2019) classifies the many sanitizers that followed and stresses that they are bug-finding tools whose value depends on the tests or fuzzing inputs that drive them.
Research compilers went further and enforced complete safety on unmodified C. SoftBound attaches base and bound metadata to every pointer and checks each dereference (Nagarakatte et al., 2009). Its companion, CETS, adds identifiers that detect access to freed objects (Nagarakatte et al., 2010). Together they showed that full spatial and temporal safety for C is achievable by compilation alone, at a running-time cost that has kept the approach out of general production use.
Exploit mitigations accept that errors will occur and aim to stop them being useful. Control-flow integrity restricts indirect jumps and calls to targets permitted by the program's control-flow graph (Abadi et al., 2005). Code-pointer integrity protects only the pointers through which control can be hijacked and so achieves lower overhead (Kuznetsov et al., 2014). Such defences are widely deployed and raise the cost of attack, but they leave data-only attacks and information leaks possible. As a migration strategy, hardening buys time. It is the only option for code that nobody is in a position to change.
4. Retrofitting Safety Within C
A second family keeps the language recognisably C while giving the compiler enough information to check it. Cyclone was a safe dialect with fat pointers, region-based memory management and restrictions on unsafe constructs (Jim et al., 2002). Porting existing code to it required manual changes, and it remained a research system, though its treatment of regions influenced the design of Rust. CCured took the opposite approach to effort: it analysed existing C, inferred which pointers were used safely, and inserted runtime checks only for the rest (Necula et al., 2002).
Checked C is the most recent representative. It extends C with pointer types whose bounds are declared by the programmer and verified by the compiler, statically where possible and dynamically otherwise, and it lets checked and unchecked code coexist in one file (Elliott et al., 2018). The design is aimed squarely at incremental migration: a function can be converted, compiled and tested while the rest of the program is untouched. The limitation shared by this family is that the guarantee holds only inside converted regions and concerns spatial safety first. Modern C++ offers a comparable path through resource-owning types and smart pointers, which reduce temporal errors by convention but are not enforced by the language.
5. Enforcing Safety in Hardware
Capability architectures move the check into the processor. In CHERI, a pointer is replaced by a capability that carries bounds and permissions and is protected by a tag that software cannot forge; every load and store is checked against it (Woodruff et al., 2014). Existing C and C++ can in large part be recompiled for such a machine, with source changes concentrated in code that treats pointers as integers. The attraction for legacy systems is plain: spatial safety arrives through the compiler and the hardware, without rewriting. The obstacles are equally plain. The hardware must exist in the products concerned, capabilities are wider than conventional pointers, and temporal safety needs further machinery such as revocation of stale capabilities. Hardware enforcement also turns a violation into a fault, which stops an attack but still crashes the program.
6. Incremental Replacement With Rust
Rust provides memory safety without a garbage collector through a type system built on ownership and borrowing: every value has one owner, references are either shared and read-only or unique and mutable, and the compiler rejects programs that could alias and mutate at once (Matsakis and Klock, 2014; Jung et al., 2021). Operations the compiler cannot verify are confined to blocks marked unsafe. The RustBelt project gave a machine-checked proof that a core model of the language is sound and that important library abstractions built on unsafe code preserve safety for their callers (Jung et al., 2018).
Because Rust can call and be called through the C application binary interface, a legacy program can be migrated one component at a time. The Servo project demonstrated the approach on a browser engine and reported how ownership caught errors that are routine in comparable C++ code (Anderson et al., 2016). Tock showed that an operating system kernel for microcontrollers could use the type system to isolate drivers at no runtime cost (Levy et al., 2017). The Linux kernel has since accepted Rust as a language for new drivers, which brings the approach to one of the largest C codebases in existence.
The empirical literature tempers the picture in three ways. First, unsafe code is common where Rust meets C. Astrauskas et al. (2020) found that unsafe blocks in published libraries are mostly small and are used largely to call foreign or other unsafe functions, and Evans et al. (2020) showed that many libraries which contain no unsafe code themselves depend on others that do. Second, the bugs that remain cluster there: in the study of real-world Rust defects by Qin et al. (2020), the memory safety bugs examined all involved unsafe code. Third, adoption has human costs. Developers interviewed and surveyed by Fulton et al. (2021) valued the confidence Rust gave them and reported a steep learning curve, slow compilation and friction in interoperating with existing code.
7. Automated Translation
Hand rewriting is slow, which makes mechanical translation attractive. Current tools translate C syntax into Rust that preserves the behaviour of the original by using raw pointers and unsafe blocks throughout. The output compiles with the Rust toolchain but is no safer than its source. Emre et al. (2021) examined why, and used the compiler's own borrow-checking errors to rewrite some raw pointers into safe references. They found that only a modest fraction could be lifted this way, because C programs depend on aliasing patterns and pointer arithmetic that the ownership discipline forbids. Translation is therefore best understood as a first step that changes the language and the build, after which the hard work of restructuring data ownership remains to be done by people or by better tools.
8. Choosing and Combining Strategies
The families differ along two dimensions that matter to a project planning migration: how much must be done to the existing code, and how strong a guarantee results. Figure 1 places them on those axes. No quadrant dominates. Mitigations are cheap and partial. A rewrite is costly and strong. Capability hardware offers strength at low source cost but depends on the platform, and dialects and translation sit in between.

In practice the strategies are layered. A plausible plan hardens the whole codebase at once, writes new components in a safe language, and selects existing components for rewriting by exposure: parsers of untrusted input, protocol handlers and code with a history of vulnerabilities come first. The government guidance recommends this kind of prioritised roadmap in place of wholesale replacement (CISA et al., 2023). Whatever the target, the advice of the legacy code literature applies. Behaviour should be pinned down with characterisation tests before a component is replaced, and the seams at which a component can be detached have to be found or created first (Feathers, 2004).
9. Gaps and Future Work
Several questions that a migration planner would ask are poorly answered in the literature. The first is cost. Published accounts describe what was migrated and what was learned, but controlled evidence on the effort of rewriting, annotating or translating a given volume of C is scarce, as is evidence on defect rates in migrated components over time. The second is the boundary. A mixed-language program has an interface at which Rust's assumptions about aliasing and lifetimes must be upheld by C code that knows nothing of them, and tools for checking that interface are immature compared with tools for either language alone.
The third gap concerns the residue. Every realistic plan leaves a body of C and C++ in service for many years, and research on making that residue safer, through better sanitizers in production, capability hardware or checked dialects, receives less attention than the target language. Finally, most studies of Rust in use examine open-source libraries. Evidence from safety-critical, embedded and regulated settings, where certification and toolchain qualification govern language choice, would widen the basis for the policy now being made.
10. Conclusion
Memory safety can be approached from an existing C or C++ codebase by four routes: harden what exists, annotate it into a checked dialect, run it on hardware that enforces bounds, or replace it piece by piece with code in a safe language. The literature shows each to be feasible and none to be sufficient alone. Rust offers the strongest guarantee for new and rewritten code, with the caveat that safety must be argued, not assumed, wherever unsafe blocks and foreign interfaces appear. The older strategies keep their place because most legacy code will be migrated late or never. Sound planning treats migration as a portfolio ordered by risk, and the research most needed is on its costs and on the interfaces between safe and unsafe worlds.
Conflict of interest
To be declared by the author(s) before publication.
Funding
To be declared by the author(s) before publication.