Memory Safety and Memory-Safe Languages
1. Overview
Definition: Memory safety is the property that guarantees a program never accesses memory outside the spatial bounds and temporal lifetime of the regions it has been allocated. A memory-safe program reads and writes only regions that are validly allocated, not yet freed, and consistent with their type.
C and C++ deliver high performance and direct hardware control through pointer arithmetic and manual memory management, but in exchange the programmer bears full responsibility for bounds checking and lifetime management. The problem is that discharging this responsibility perfectly is practically impossible for a human. Mistakes such as writing one byte past the end of a buffer (off-by-one) or dereferencing a pointer to already-freed memory (use-after-free) survive code review and testing to reach production. Such residual defects are not mere bugs; they become the starting point for remote code execution (RCE), through which an attacker hijacks the program's control flow.
The reason this problem has been elevated to an industry-wide agenda is that the statistics are overwhelming. Microsoft reported that roughly 70% of the security vulnerabilities (CVEs) disclosed each year in its products are memory safety issues, and Google has repeatedly confirmed similar ratios in Chromium and Android. In other words, memory safety is not one vulnerability type among many but the structural source of high-severity vulnerabilities. Because patching individual defects leads to endless recurrence, the field is shifting toward "eliminating vulnerabilities at the source."
Memory safety deserves three premises when discussed. First, it is a binary guarantee — if even one spot is unsafe, that gap can expose the entire process memory, so "90% safe" is close to being not safe at all. Second, there is an inherent tension with performance — bounds and lifetime checks demand a cost in some form, so each solution differs in where it moves that cost (runtime, compile time, or hardware). Third, there is heavy legacy dependence — the world's core infrastructure is written in C/C++, so a solution must cover not only "how to build anew" but also "how to harden the existing," to be realistic.
Memory safety is particularly hard to handle because it is a non-functional security quality attribute rather than a functional requirement. It does not surface in the features users see, so it is easily deprioritized, yet once breached it collapses the trustworthiness of the whole system. The reason memory safety is such a heavy task is that its defects lie silently dormant. A functional bug surfaces immediately as a wrong screen or an exception, but a memory safety violation runs fine normally and breaks only under specific inputs or timing. A write that crosses the boundary by one byte subtly corrupts an adjacent variable, and the result erupts belatedly on a completely different code path. Because of this non-locality, debugging is extremely difficult and it is easy to miss even with high test coverage. This very characteristic defines the limits of an approach that relies on human attentiveness.
That shift was formalized into government- and standards-level recommendations between 2022 and 2024. The U.S. NSA's "Software Memory Safety" information sheet (2022), the "The Case for Memory Safe Roadmaps" by CISA, NSA, FBI and international partners (2023), and the White House Office of the National Cyber Director (ONCD)'s "Back to the Building Blocks" (2024) successively recommended a transition to memory-safe languages. From a professional engineer's perspective, the essence of this topic is not "how to catch bugs better" but a paradigm shift in the design of the entire software supply chain, spanning languages, runtimes, hardware, and organizational policy.
2. The Structure and Types of Memory Safety Violations
To understand memory safety violations, one must first divide them along two axes. Spatial safety is the question of "are we accessing within the permitted range," while temporal safety is the question of "are we accessing at a still-valid moment." If buffer overflow is the representative of the former, use-after-free is the representative of the latter. Actual vulnerability types derive from the intersection of the two axes.
This distinction matters because the two safeties are mutually independent, so guarding only one is not complete. No matter how thorough the bounds checking (spatial), it cannot stop a temporal violation that uses an already-freed pointer; conversely, even with perfect lifetime management, exceeding an index range produces a spatial violation. Therefore the guarantee of "memory safety" holds only when both axes are satisfied simultaneously, and any solution must be evaluated by how it handles both aspects together.
flowchart TD
MS["Memory safety violation"]
MS --> SP["Spatial violation (out of bounds)"]
MS --> TP["Temporal violation (lifetime breach)"]
SP --> OF["Buffer over/underflow"]
SP --> OOB["Out-of-bounds read/write (OOB)"]
SP --> TC["Type confusion"]
TP --> UAF["Use-after-free"]
TP --> DF["Double free"]
TP --> UI["Uninitialized read"]
OF --> EXP["Control-flow hijack / info leak"]
OOB --> EXP
TC --> EXP
UAF --> EXP
DF --> EXP
UI --> EXP
The key in the structure above is that several seemingly different vulnerability types ultimately converge on one result (an attacker's arbitrary read/write of memory). Put differently, an attacker who makes any one of these types succeed gains the same attack power. It is useless for the defense to block only one type; the structural reason a comprehensive guarantee sealing both safety axes is needed lies here. Spatial violations arise when the bounds of an array or buffer cannot be kept. For example, copying a long string into a short stack buffer with strcpy without a length check can overwrite the adjacent return address, jumping to attacker-chosen code when the function returns. This is the classic stack overflow that has recurred for nearly half a century since the 1988 Morris worm.
Spatial violations are especially dangerous because data and control information are intermixed in the same memory space. The stack holds not only local variables but also the function's return address and saved registers, and on the heap user data sits adjacent to the allocator's metadata. The moment a boundary-crossing write touches this control information, simple data corruption leaps into a hijack of program flow. This fact — that memory layout determines the attack surface — is the starting point for many mitigations that try to randomize or separate the layout.
Temporal violations come from the failure of lifetime management. Use-after-free is the case of reusing a pointer (dangling pointer) to memory that has been returned via free(). If the freed chunk is reallocated to another object and then accessed through the old pointer, the attacker can manipulate one object's contents with another object's pointer. This is why use-after-free is especially fatal in large programs such as web browsers where object lifetimes are intricately intertwined. The following table summarizes the main types along with typical causes and impacts.
| Type | Class | Typical cause | Main impact |
|---|---|---|---|
| Stack/heap buffer overflow | Spatial | Missing length check, off-by-one | Control-flow hijack, RCE |
| Out-of-bounds read (OOB read) | Spatial | Missing index validation | Info leak (e.g., Heartbleed) |
| Use-after-free (UAF) | Temporal | Dangling pointer reuse | Object manipulation, RCE |
| Double free | Temporal | Duplicate free, ownership confusion |
Heap metadata corruption |
| Type confusion | Spatial/type | Incorrect casting | Arbitrary read/write |
| Uninitialized memory use | Temporal | Missing initialization | Info leak, non-deterministic behavior |
As the table shows, the manifestations of impact differ but the root is the same. All of them stem from "a pointer looking at memory being out of sync with the actual object's bounds or lifetime." Therefore the fundamental countermeasure also converges — not per-type patches but a direction where a language or hardware enforces the alignment of pointer and object.
Meanwhile, use of uninitialized memory is less conspicuous but a quiet channel for information leaks. If a buffer that was only allocated but not initialized is sent outside as is, other data that previously occupied that spot (e.g., leftover cryptographic keys) may leak along with it. It is also characteristic that, being non-deterministic, it is hard to reproduce in testing.
That these types are not abstract risks is proven by real large-scale incidents. In 2014, OpenSSL's Heartbleed (CVE-2014-0160) leaked even private keys and sessions from server memory through a single out-of-bounds read (OOB read) that failed to validate a length field, triggering a worldwide reissuance of HTTPS infrastructure. In 2017, Windows SMB's EternalBlue exploited a buffer-overflow-class defect to become the conduit for the WannaCry and NotPetya ransomware pandemics. Both incidents are cases where "one boundary mistake in a line of memory" was amplified into nation- and industry-scale damage, showing why memory safety must be treated not as an individual bug but as a systemic risk.
3. Exploit Mechanisms and Mitigation Techniques
A memory defect does not immediately mean a successful attack. There are several stages between a vulnerability and an actual exploit, and the defense erects barriers in between. The attacker uses the defect to (1) overwrite code or pointers, (2) bypass defensive techniques, and (3) execute the actual malicious behavior; the defense places mitigations at each stage to raise the cost of attack. This "cost-raising" perspective is the core frame that explains why mitigations alone are not enough. Below is a detailed architecture showing a typical exploit chain and the corresponding mitigation layers.
flowchart LR
subgraph ATK["Exploit chain"]
V["Discover memory defect"] --> OW["Obtain arbitrary memory write"]
OW --> HJ["Hijack control flow"]
HJ --> PAY["Execute payload (ROP / shellcode)"]
end
subgraph DEF["Mitigation layers (defense in depth)"]
C1["Stack canary"]
C2["DEP / NX (no-execute)"]
C3["ASLR (address randomization)"]
C4["CFI (control-flow integrity)"]
end
OW -. "detect" .-> C1
PAY -. "block" .-> C2
HJ -. "disrupt" .-> C3
HJ -. "verify" .-> C4
For example, in a typical stack overflow, a copy without a length check writes 128 bytes into a 64-byte buffer, overwriting the adjacent stored return address, and the instant the function returns it branches to the address the attacker planted. Mitigations try to break each link of this chain. In this chain, the mitigation techniques are probabilistic barriers that do not block the attack at the source but raise its cost. A stack canary places a random value in front of the return address so that if an overflow changes that value, execution is halted before the function returns. DEP/NX marks data regions as non-executable to block direct execution of shellcode injected onto the stack. Attackers then bypassed this with ROP (Return-Oriented Programming), chaining existing code fragments (gadgets), to which ASLR responded by randomizing addresses to make gadget locations hard to predict.
The problem is that this back-and-forth never ends. ASLR is neutralized when addresses leak via information disclosure (OOB read), and CFI is not complete due to performance costs and bypass techniques. That is, mitigation techniques are after-the-fact and probabilistic, so once an attacker secures a single powerful arbitrary read/write primitive, they can collapse in sequence. In fact, at hacking contests like Pwn2Own, even the latest browsers equipped with all these mitigations are repeatedly breached by exploit chains starting from a single memory defect. This limitation justifies the shift to language- and hardware-level approaches that "do not create the defect in the first place."
Meanwhile, techniques that discover defects in advance are also one axis of the mitigation system. Coverage-guided fuzzing injects a mass of random/mutated inputs to explore abnormal paths, and combining it with a dynamic analyzer like AddressSanitizer (ASan) makes overflows and UAF surface immediately as actual crashes. Google's OSS-Fuzz has automatically found tens of thousands of defects in major open source this way. However, fuzzing only finds defects on paths it reaches, so it cannot guarantee completeness — the fact that testing can reduce defects but cannot prove their "absence" is the reason the move to safe languages is a fundamental cure rather than a complement.
4. Comparison of Approaches to Achieving Memory Safety
If the preceding mitigations make an attack "difficult," the safety-assurance approach aims to make the defect itself "impossible." There are broadly three paths to achieving memory safety. The first is runtime-checking safe languages, where, like Java, C#, Go, and Python, a garbage collector (GC) automatically manages lifetimes and inserts bounds checks on every array access. Productivity and safety are high, but there is GC-induced latency and memory overhead, so it may be unsuitable for OS kernels or real-time systems. GC removes dangling pointers at the source by having the runtime track "when to reclaim memory," but that tracking cost and the unpredictable stop-the-world collection are the trade-offs. This is why GC tuning becomes a constant task in areas where tail latency matters, such as financial trading or game servers.
The second is Rust, a compile-time-verification safe language. Without a GC, Rust uses a borrow checker in which the compiler statically verifies the rules of ownership, borrowing, and lifetime, enforcing that each value has exactly one owner and that a mutable reference and immutable references cannot coexist simultaneously. This blocks use-after-free and data races at the source with no runtime cost. That said, low-level code that must step outside the ownership model is explicitly separated into unsafe blocks to narrow the verification responsibility.
Rust's safety lies in "turning the impossible into a compile error." For example, code that tries to modify a collection while iterating over it — code that in other languages would lead to iterator invalidation at runtime — fails to compile at all in Rust because it violates the rule against coexistence of mutable and immutable references. Because defects are thus filtered out at the development stage before reaching production, the responsibility for safety shifts from human attentiveness to the tool's guarantee. This is why Rust is called "zero-cost abstraction" — it pays the price of safety not in runtime performance but in compile time and design constraints.
The third is hardware-supported safety. As an approach that reinforces safety without rewriting existing C/C++ assets, Arm's Memory Tagging Extension (MTE) and CHERI/Morello are representative. The following table compares the three approaches by criteria.
| Criterion | Runtime check (Java/Go) | Compile time (Rust) | Hardware support (MTE/CHERI) |
|---|---|---|---|
| Point of safety guarantee | During execution | At compile | During execution (HW-enforced) |
| Performance overhead | GC / check cost | Nearly none | Low–medium (varies by design) |
| Reuse of existing C/C++ | Hard (rewrite) | Hard (rewrite) | Easy (recompile / harden) |
| Learning / transition cost | Low–medium | High (ownership) | Medium (toolchain / HW) |
| Application area | Apps / servers | Systems / kernels / apps | Legacy hardening |
What matters in the table's comparison is the reason the differences arise. Rust has almost no performance overhead because it moved the safety checks from runtime to compile time, and in exchange the learning cost of developers mastering the ownership rules is high. Conversely, the hardware-supported approach is advantageous for legacy hardening because it can attach tags to pointers and verify them by recompilation alone without source rewriting, and in exchange it requires dedicated CPU features and memory-tag storage overhead. Rather than any single one being absolutely superior, the realistic solution is to combine them strategically according to the target system's performance budget, legacy scale, and team capability.
In practice, approaches are mixed even within a single system. For example, a performance-critical crypto/network core can be rewritten in Rust, a vast legacy C library hardened with MTE, and the upper business logic left in a GC language — a layered differential application. The crux here is the boundary (FFI, foreign function interface) where modules with different safety guarantees meet. The moment a safe language calls unsafe code, the guarantee is severed, so minimizing the boundary and intensively verifying and documenting that point governs overall safety.
5. Deep Dive: Policy, Hardware, and Industry Adoption Trends
Memory safety is no longer an internal quality issue within development teams but an axis of ecosystem transition that policy, hardware, and industry move together on. Over the past three years, memory safety has entered the realm of national policy and regulation beyond technical discourse. The "Back to the Building Blocks" report released by the White House ONCD in February 2024 recommends that software manufacturers adopt memory-safe languages to "eliminate entire classes of vulnerabilities," offering C and C++ as examples of memory-unsafe languages and Rust as an example of a safe language. This connects to the flow in which the earlier "The Case for Memory Safe Roadmaps" (2023) by CISA, NSA, FBI and international partners asked manufacturers to establish a memory safety roadmap for existing products. "Exploring Memory Safety in Critical Open Source Projects," released by CISA and others in June 2024, analyzed 172 critical open source projects and pointed out that a majority are still written in C/C++ and that even projects written in safe languages can be exposed through unsafe dependencies. (The specific level of implementation/enforcement is subject to change, so consulting the originals is necessary.)
The trend on the hardware side is also clear. Arm's Memory Tagging Extension (MTE) is a "lock-and-key" scheme that divides physical memory into 16-byte granules, assigns a 4-bit tag to each granule, loads the same tag onto pointers, and has hardware check tag matching on access. If tags mismatch, it immediately detects spatial/temporal violations. Google reported that it caught a substantial number of memory bugs before release by leveraging MTE in Android (a vendor-side figure, so interpret with care). The more fundamental approach, CHERI, is evaluated on Arm's experimental platform Morello, replacing ordinary pointers with unforgeable hardware capabilities to imprint bounds and permissions onto the pointer itself.
This hardware hardening plays the role of filling the gap in the software transition. Rewriting in a safe language takes years, but during that interval the legacy remains exposed to attack. A hardware feature like MTE applies directly to existing binaries to mitigate this "transition-period risk," and in that sense it complements the software approach along the time axis rather than competing with it.
The philosophical difference between the two hardware approaches is also worth savoring. MTE is probabilistic and incremental — a 4-bit tag has only 16 possibilities, so a chance of accidental matching remains, but it can be applied broadly by recompilation alone without source modification, fitting the practical goal of "hardening the legacy now." By contrast, CHERI is deterministic and fundamental — it seals off bounds at the source with capabilities, but it changes the ABI and pointer representation, requiring a redesign across the ecosystem. This contrast shows that security is always a balance between "completeness" and "adoptability," suggesting that the two approaches may coexist complementarily for the time being.
Industry adoption cases show a strategy of "safe from new code" rather than "rewrite everything." Google, after shifting to a policy of applying Rust first to newly written code in Android, announced that the proportion of memory safety vulnerabilities in new code dropped significantly. The key insight is that because vulnerabilities concentrate mostly in new code and recent changes, one can bend the overall vulnerability curve just by changing the language of new code, without porting decades-old legacy in full. Microsoft (introducing Rust in parts of the Windows kernel), AWS (Firecracker, etc.), and the Linux kernel (Rust for Linux) are following the same gradual transition path.
This strategy is effective for a quantitative reason. Defect density tends to be inversely proportional to the "age" of the code, so old, stable code has already had countless bugs filtered out, whereas defects concentrate in newly added code. Therefore, applying a safe language only to new code still reduces both the proportion of unsafe code in the whole codebase and new vulnerabilities over time. This insight — that the risk curve can be bent without the unrealistic goal of a full rewrite — is the theoretical foundation of a pragmatic transition strategy.
This trend is gradually being reflected in the domestic (Korean) context as well. A large share of core systems in the public and financial sectors consist of C/C++-based legacy and embedded firmware, so, coupled with the strengthening of software supply chain security (SBOM mandates, security weakness diagnosis, etc.), memory safety is emerging as a mid-to-long-term quality/security metric. However, since a transition to safe languages must be backed by workforce proficiency and a verification system, it is reasonable for domestic organizations to prioritize selective application to new modules / high-risk components and the internalization of static analysis and fuzzing over a full-scale transition.
6. Considerations and Implications
Memory safety is not a task that ends with the introduction of a single tool but, from a professional engineer's perspective, a strategic agenda requiring a multi-year transition roadmap and trade-off management. The considerations below extend beyond technology choice into the organization's investment, governance, and supply chain strategy.
- Gradual transition strategy (avoiding wholesale rewrite): Porting millions of lines of legacy at once has excessive cost and risk. A realistic path is a strangler approach that writes new and high-change modules first in a safe language such as Rust while clearly managing boundary points (FFI). Organizations should set per-product memory safety roadmaps and KPIs (e.g., the proportion of safe language in new code).
- Quantitative management of the performance/productivity trade-off: GC-language latency, Rust's learning curve, and the hardware approach's tag overhead each impose different costs. One should apply approaches differentially by distinguishing latency-sensitive kernel/real-time areas, business apps where productivity matters, and areas where legacy hardening is urgent, and make decisions by quantifying costs with benchmarks.
- Expansion from a supply chain/dependency perspective: Even if one's own code is switched to a safe language, a defect in a C/C++ library or dependency leaves exposure. A strategy must proceed in parallel to make the language/safety of dependencies visible via SBOM (software bill of materials) and to isolate unsafe dependencies with safe-language bindings or sandboxing.
- Parallel use with defense in depth (before full transition): Since transition takes time, during that period one must run in parallel mitigation techniques such as ASLR, CFI, stack canaries, and MTE along with testing based on fuzzing, static analysis (SAST), and AddressSanitizer to manage residual risk. Adopting a safe language does not replace mitigation techniques but reduces their burden at the source.
- Workforce/organizational capability and regulatory response: A new paradigm such as the ownership model entails education/hiring costs. At the same time, since government procurement and security certification increasingly require memory safety roadmaps, regulatory response and capability internalization should be bundled as simultaneous tasks and reflected in mid-to-long-term plans.
- Measurable goals and governance: The declaration "we aim for memory safety" alone does not advance the transition. As the ONCD report emphasizes, measurability is key, and only when metrics such as the proportion of safe language in new code, the trend of memory safety CVEs, and the number of safe/unsafe boundaries (FFI) are defined and included in the management reporting system is the transition tracked and managed at the organizational level.
- Caveats in the age of AI code generation: In an environment where generative AI pours out code in bulk, there is a risk that unsafe patterns are reproduced because of the vast C/C++ idioms included in the training data. AI-generated code should also be placed under the same static analysis, fuzzing, and language policy control, so that guardrails are designed to prevent productivity gains from turning into safety regressions.
References
- The White House ONCD, "Back to the Building Blocks: A Path Toward Secure and Measurable Software" (2024): https://www.whitehouse.gov/oncd/briefing-room/2024/02/26/press-release-technical-report/
- CISA/NSA/FBI et al., "The Case for Memory Safe Roadmaps" (2023): https://www.cisa.gov/resources-tools/resources/case-memory-safe-roadmaps
- NSA, "Software Memory Safety" (2022): https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI_SOFTWARE_MEMORY_SAFETY.PDF
- Arm, "Memory Tagging Extension (MTE)": https://developer.arm.com/documentation/108035/latest/
- CHERI / Morello project (University of Cambridge): https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/
In one line: Memory safety is the property ensuring a pointer never drifts out of sync with an object's bounds and lifetime; it is the root problem behind roughly 70% of severe vulnerabilities, and the key strategy is a gradual transition that combines runtime checking (Java/Go), compile-time verification (Rust), and hardware support (MTE/CHERI) to fit the situation.