Would Memory-Safe Languages Solve All of Our Problems?
It has been estimated that over 70% of CVEs from Microsoft, Mozilla, and Google stem from memory-related issues. Memory overflow vulnerabilities more than doubled (844 to 1,881) between 2013 and 2022, while memory corruption flaws surged nearly 6x (577 to 3,420) within the same period.
C and C++ are two of the most widely used programming languages in history and still form the backbone of the world’s most essential, high-performance, and security-critical software systems. Unlike languages with automatic memory management (known as garbage collection), such as Java or Python, C and C++ require manual memory management, meaning developers must explicitly allocate and deallocate memory. This low-level control, while powerful, can lead to serious memory-related security vulnerabilities—such as buffer overflows or use-after-free errors—if not handled correctly. Windows, primarily written in C/C++, releases approximately 12 major security update packages per year, in addition to occasional out-of-band patches for urgent vulnerabilities, with around 400-800 memory-related fixes contained in these patches.
So how do memory-related vulnerabilities work, and would using a memory-safe language like Rust prevent a lot of these vulnerabilities?
What is a Memory-Related Vulnerability?
In its simplest form, a memory-related vulnerability (MRV) is a software flaw that arises from the unsafe handling of computer memory. These bugs can lead to crashes, data leaks, or even allow attackers to execute arbitrary code—which is often how serious exploits like Remote Code Execution (RCE) happen.
MRV Example: Buffer Overflow
#include <stdio.h>
#include <string.h>
void vulnerable_function(const char *input) {
char buffer[16]; // only 16 bytes
// ❌ unsafe: doesn't check length of input!
strcpy(buffer, input); // potential buffer overflow
printf("You entered: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <your_input>\n", argv[0]);
return 1;
}
vulnerable_function(argv[1]);
return 0;
}
How It’s Vulnerable:
buffer[16]is statically allocated.strcpy(buffer, input)copies user input without checking length.- If you pass an argument longer than 15 characters (plus null terminator), you overflow the buffer.
- This can overwrite return addresses or adjacent variables.
Safer Replacement:
To fix it, use strncpy() or memcpy() with bounds checking:
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // manually null-terminate
// Or, if reading from stdin, use:
fgets(buffer, sizeof(buffer), stdin);
In C, unsafe functions like strcpy, sprintf, or unchecked array writes still regularly cause real-world CVEs—especially in embedded systems, firmware, and legacy enterprise code.
So How Does Unsafe Memory Allocation Lead to RCE?
Step-by-Step: How Unsafe Memory Handling Leads to RCE
- Unsafe Memory Handling (e.g., Buffer Overflow): A function allocates memory (
char buffer[128]), but user-controlled input exceeds that size (say, 512 bytes). The overflow can overwrite important control structures, such as function return addresses, function pointers, or heap metadata. - Control Flow Hijack: By crafting input precisely, the attacker can overwrite the return address or other instruction pointers. When the function finishes and tries to return, it jumps to an attacker-supplied address. That address may point to:
- Malicious shellcode embedded in the input (if the stack is executable)
- A ROP chain (Return-Oriented Programming) if it's not
- Execute Attacker-Controlled Code (RCE): Now the program is executing code that the attacker wrote or assembled, within the memory of a privileged service. This code can:
- Spawn a shell
- Download more payloads
- Exfiltrate data
- Laterally move across the network
So Why Doesn’t Every Memory Error Lead to RCE?
| Memory Bug Type | Can Lead to RCE? | Why? |
|---|---|---|
| Null pointer dereference | ❌ | Usually just crashes the app. |
| Out-of-bounds read | ⚠️ | Can leak data (information disclosure), but not code execution by itself. |
| Buffer overflow (write) | ✅ | Can overwrite control flow data. |
| Use-after-free | ✅ | Can be used to hijack virtual function tables or pointers. |
| Heap overflow | ✅ | If allocator metadata is corrupted, attacker can redirect execution. |
EternalBlue & WannaCry — A Real-World Example
EternalBlue exploited a buffer overflow in SMBv1 (srv.sys)—a vulnerable driver in Windows. The bug was a type confusion and memory corruption vulnerability in how SMB handled certain message headers. It allowed an attacker to write outside of a buffer in kernel memory.
Timeline of the Exploit:
- Attacker sends a specially crafted SMB packet to port 445 (no authentication needed).
- The packet triggers a buffer overflow in the kernel.
- Attacker's payload is sprayed into memory (ROP + shellcode).
- The exploit overwrites a function pointer or return address in the kernel.
- Execution jumps to the attacker's code → RCE with SYSTEM privileges.
- WannaCry then installed a ransomware binary, encrypted files, and used EternalBlue again to self-propagate to other hosts.
What Made EternalBlue So Dangerous?
- Exploited unauthenticated SMBv1 service → reachable from WAN/LAN.
- Gained kernel-level RCE → SYSTEM access.
- Self-replicating worm → no user interaction needed.
- Worked on many unpatched Windows systems.
Common Defenses & Why They Don't Always Work:
- What if the memory is non-executable? Exploits use Return-Oriented Programming (ROP)—chaining existing code to avoid injecting new executable code.
- Isn’t ASLR (Address Space Layout Randomization) supposed to stop this? It helps, but attackers can use information leaks to defeat it or brute-force if entropy is low.
- What about stack canaries? They detect some overflows, but not all. Attackers bypass them with heap bugs, use-after-free, or format string vulnerabilities.
- Why are these bugs still around? Many enterprise and embedded systems still run legacy C/C++ code with manual memory management. It’s hard to get rid of completely.
Would Using a Memory-Safe Language Get Rid of These Vulnerabilities?
Memory-Safe Language vs. Garbage-Collected Languages
Memory Safety: A memory-safe language prevents classes of memory errors like:
- Buffer overflows
- Use-after-free
- Null dereferences
- Double-free Examples: Rust, Go, Swift (and Java, but more via garbage collection). Memory-safe languages enforce safety through rules at compile time or runtime. For example:
- Rust uses ownership and borrowing rules to guarantee safety at compile time—no garbage collector required.
- Swift uses Automatic Reference Counting (ARC) with runtime checks.
- Go uses a garbage collector, but also restricts pointer usage.
Garbage Collection: A garbage-collected language automatically finds and frees memory that’s no longer used. Examples: Java, Python, Go, C#. These reduce memory leaks and dangling pointer bugs, but not necessarily buffer overflows or bounds errors.
Would Memory-Safe Languages Eliminate Memory Vulnerabilities?
Refactoring legacy codebases into memory-safe languages would eliminate the majority of memory-related vulnerabilities—which account for roughly 70% of serious security bugs in large C/C++ codebases. However, this process requires significant engineering effort and organizational investment. Memory-safe languages could prevent:
- Buffer overflows
- Heap corruptions
- Use-after-free
- Double-free
- Dangling pointers
So Why Doesn’t Every Organization Switch Right Now?
- Legacy Codebases: Most critical infrastructure is in C/C++ (OS kernels, drivers, embedded systems). Rewriting millions of lines in a new language is expensive and risky.
- Developer Expertise: Many developers and teams are trained in C/C++. Rust or Go have steeper learning curves, especially Rust’s borrow checker.
- Cost & Time: Porting is an expensive, long-term effort. Regressions and bugs during rewrites are a real risk.
- Third-Party Library Ecosystems: Many newer languages lack mature libraries or bindings compared to C/C++.
- Performance & Hardware Access: In performance-critical or low-level environments (drivers, firmware), only C/C++ offer the necessary control. Rust can match this but is newer and requires care to integrate.
Conclusion
While memory-safe languages like Rust, Go, and Swift offer a compelling solution to a significant portion of modern cybersecurity vulnerabilities by preventing entire classes of memory errors, they are not a silver bullet. The immense challenge of migrating vast legacy codebases written in C/C++, particularly in critical infrastructure, presents substantial financial, logistical, and expertise hurdles. Performance requirements, existing developer skill sets, and the maturity of third-party ecosystems also play a crucial role in slowing widespread adoption. Therefore, a multifaceted approach is likely to persist for the foreseeable future. This includes continued investment in static analysis tools, runtime protections, fuzzing, and secure coding practices for C/C++ development, alongside the gradual adoption of memory-safe languages for new projects and strategic rewrites where feasible and beneficial. Ultimately, reducing memory-related vulnerabilities will be an ongoing effort that balances the proven benefits of memory-safe languages with the practical realities of a complex software landscape.