Memory Safety Without Rust: How C++, Go, Python, and Java Each Approach the Problem and Where Each Falls Short
Post-quantum security, the Linux kernel’s Rust adoption, and NSA/CISA memory safety advisories have turned this into a board-level conversation. Yet a clear, language-by-language breakdown of what each runtime actually guarantees remains frustratingly rare. This article closes that gap.
Memory safety vulnerabilities are not just an academic footnote. In 2019, Microsoft’s Security Response Center famously disclosed that roughly 70% of all CVEs they fix each year trace back to memory unsafety in C and C++. Google has reported similar figures for Chrome. And in 2022, the NSA published a Cybersecurity Information Sheet urging organisations to transition to memory-safe languages — naming Go, Java, Python, and Rust as preferable alternatives to C and C++.
That guidance, however, is where the nuance often gets lost. The phrase “memory-safe language” covers a wide spectrum. A language can prevent use-after-free bugs but still allow logic-level data races. It can eliminate null-pointer dereferences in most cases but leak memory freely. Understanding exactly where each language draws the safety line is, therefore, essential — especially now that Rust’s kernel adoption is raising the bar for what “safe” can mean.
So let’s walk through each language — C++, Go, Python, and Java — look at what safety mechanisms they actually provide, where those mechanisms break down, and what the tradeoffs look like in practice.
The Unsafe Baseline
C++: Power With a Price Tag
C++ remains the language of operating systems, game engines, financial infrastructure, and embedded systems. Its performance ceiling is essentially unmatched. But that power comes from giving developers direct control over memory allocation — and that control is also the root of its most serious vulnerabilities.
The core memory safety issues in C++ fall into a familiar taxonomy: buffer overflows, use-after-free errors, double-free corruption, null-pointer dereferences, and dangling pointer reads. All of these are undefined behaviour in the C++ standard, which means the compiler may — and often will — generate code that optimises around the assumption they never happen, with unpredictable results at runtime.
Importantly, the C++ community has been working hard on this problem for years. Modern C++ (C++11 onward, with significant additions through C++23) introduced smart pointers — std::unique_ptr, std::shared_ptr, std::weak_ptr — that automate ownership and dramatically reduce the manual new/delete footgun. The C++ Core Guidelines, jointly developed by Bjarne Stroustrup and Herb Sutter, provide a comprehensive set of modern best practices. Tools like AddressSanitizer, MemorySanitizer, and the Clang static analyser catch many issues at compile time or test time.
But — and this is the critical point — none of these are enforced by the language itself. A developer can mix smart pointers and raw pointers freely. They can call .get() on a smart pointer and pass a raw pointer to a C API, invalidating the ownership model entirely. The compiler will not stop them. Furthermore, templates and metaprogramming can produce deeply complex lifetimes that even experienced engineers struggle to reason about correctly.
The Core Problem: C++ safety tools are opt-in at the developer and organisation level. A single legacy dependency, a rushed patch, or a misuse of
reinterpret_castcan reintroduce vulnerabilities that the rest of the codebase works hard to prevent. There is no enforced safety perimeter.
The Clang “safe buffers” profile, proposed as part of the broader C++ Profiles initiative, aims to make bounds-checking the default. Microsoft’s Azure team has also been piloting C++ code reviews with memory safety in mind. These are meaningful steps, yet they remain ecosystem-level conventions rather than language-level guarantees.
The following snippet demonstrates a classic use-after-free — still valid, compilable C++ — that a modern compiler may not warn about unless sanitizers are active at build time:
// Compile with: g++ -fsanitize=address -o demo demo.cpp
// Without -fsanitize=address, this may silently corrupt memory.
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr = std::make_unique<int>(42);
int* raw = ptr.get(); // raw pointer — bypasses ownership
ptr.reset(); // destroys the object
std::cout << *raw << "\n"; // use-after-free: undefined behaviour
return 0;
}
Run this with AddressSanitizer (-fsanitize=address) and it correctly reports a heap-use-after-free. Without the sanitizer, it may silently print 42, crash, or produce garbage — entirely depending on the runtime environment.
Managed Runtimes
Go: Safe by Default, With Known Gaps
Go was designed by Google engineers explicitly to avoid the footguns of C and C++. It achieves this primarily through a garbage collector, bounds-checked slices, and the absence of pointer arithmetic in idiomatic code. You simply cannot perform ptr++ in safe Go code. Buffer overflows — at least at the language level — are not a concern because slice access is always bounds-checked at runtime.
In practice, this means that a significant category of vulnerabilities that plague C++ codebases are essentially eliminated by default. Go code does not produce use-after-free errors in normal usage, because the GC tracks object lifetimes. Null pointer dereferences are still possible (Go has nil pointers), but they produce a clean panic rather than silent memory corruption — a much more debuggable failure mode.
However, Go’s memory safety story has two important asterisks. First, the unsafe package. This is Go’s escape hatch, and it is used more widely than many developers realise — inside the standard library, in CGo bindings, and in high-performance packages that need to bypass the type system. The unsafe.Pointer type can be cast to and from any pointer type, enabling the same class of errors Go was designed to prevent.
Second — and more nuanced — Go’s goroutine model makes data races surprisingly easy to introduce. The language provides no compile-time ownership enforcement analogous to Rust’s borrow checker. The Go race detector (-race flag) catches data races at runtime, but it must be explicitly enabled and adds significant overhead — making it a testing tool rather than a production guarantee.
Important Context: Go’s race detector is excellent, but it only catches races that actually execute during a instrumented test run. Races on code paths that aren’t exercised by tests remain invisible until they manifest in production — often under high concurrency load.
The sync/atomic and sync packages provide proper synchronisation primitives, and idiomatic Go favours channels over shared memory. Nevertheless, the language does not prevent misuse. Go offers substantial safety improvements over C++ for most practical codebases, but it does not provide the formal guarantees that the term “memory-safe” sometimes implies.
Python: Safety Through Abstraction (At a Cost)
Python is, in everyday usage, about as memory-safe as a language gets. The interpreter manages all memory through reference counting (with a cyclic garbage collector on top), and developers never directly allocate or free heap memory. There are no raw pointers, no buffer overflows in pure Python code, no use-after-free errors. For application developers writing business logic, APIs, or data pipelines, Python’s memory model is simply not something you need to think about.
The nuance begins at the C extension layer. Python’s performance-critical ecosystem — NumPy, Pandas, TensorFlow, database drivers, image processing libraries — is largely implemented in C or C++. When you call numpy.array(), you are dropping into C memory management. A bug in that C layer — a buffer overflow, a use-after-free — can corrupt the Python runtime itself, producing crashes or security vulnerabilities in what appears to be pure Python code.
Additionally, Python’s Global Interpreter Lock (GIL) historically prevented true parallel thread execution, which inadvertently provided some protection against data races. However, the introduction of a free-threaded (no-GIL) mode in CPython 3.13 means that Python developers will increasingly need to reason about concurrency safety in ways they have never had to before.
There is also the matter of memory efficiency. Python objects carry substantial runtime overhead — a simple integer is not 4 bytes; it is a full Python object with a reference count, type pointer, and value, often 28 bytes or more. At scale, this memory footprint translates directly into infrastructure costs and latency, and it is the primary reason Python is rarely used for systems programming.
# This runs safely in pure Python — no memory issues.
# But if 'process_image' calls a C extension with a bug,
# the Python runtime itself can be corrupted.
def process_batch(data: list[bytes]) -> list:
results = []
for item in data:
# Safe in Python; relies on C extension safety beneath
result = process_image(item) # C extension call
results.append(result)
return results
Java: The JVM Safety Model and Its Limits
Java was one of the first mainstream languages designed with memory safety as an explicit goal — and for most of its history, that goal has been well-served. The JVM enforces strong type safety, performs automatic garbage collection, and verifies bytecode before execution. Buffer overflows, use-after-free, and pointer arithmetic are essentially impossible in standard Java code. The Java SecurityManager (though deprecated in Java 17 and removed in Java 24) added sandboxing on top.
Java’s weak point is the sun.misc.Unsafe class — now partially superseded by JEP 454 (Foreign Function & Memory API, stable in Java 22). Unsafe allows direct memory allocation, pointer arithmetic, and CAS operations that bypass the JVM’s safety checks entirely. It is widely used in high-performance libraries (Netty, Hazelcast, Cassandra’s driver, Caffeine cache) precisely because it enables performance impossible through standard Java APIs.
Beyond Unsafe, Java’s memory model has a more structural concern: garbage collection pauses. Stop-the-world GC events — though greatly reduced in modern collectors like ZGC and Shenandoah — can still cause latency spikes in memory-intensive applications. More practically, Java applications are notorious for memory pressure under load, as the JVM’s object model carries significant overhead per object.
The newer Foreign Function & Memory (FFM) API is an important evolution: it provides a safer, officially-supported path to native memory access with explicit lifetime management — an acknowledgement that Unsafe is, in practice, unavoidable for certain use cases, and that the JVM should provide a better alternative rather than pretending the need does not exist.
Vulnerability Data

The Memory Safety Matrix
Rather than making qualitative judgments, the table below maps each language against the specific memory safety properties that security researchers and compiler engineers actually measure. A “partial” rating means the protection exists but can be bypassed or is not enforced by the language itself.
| Safety Property | C++ | Go | Python | Java |
|---|---|---|---|---|
| Buffer overflow prevention | Unsafe by default | Guaranteed | Guaranteed* | Guaranteed |
| Use-after-free prevention | Partial (smart ptrs) | Guaranteed | Guaranteed* | Guaranteed |
| Null / nil pointer safety | No guarantee | Panic, not crash | AttributeError | NullPointerException |
| Data race prevention | None | Detector only (-race) | GIL (CPython ≤ 3.12) | JMM-specified |
| Integer overflow detection | UB (signed) | Wraps silently | Arbitrary precision | Wraps silently |
| Unsafe escape hatch | Always available | unsafe package | C extension layer | sun.misc.Unsafe / FFM |
| Memory leak prevention | Manual only | GC-managed | GC + refcount | GC-managed |
| Ownership / lifetime model | Convention-based | None | None | None |
| Compile-time safety enforcement | Opt-in tools only | Partial | Minimal | Partial |
* Pure Python only. C/C++ extensions operate outside these guarantees.
Runtime Overhead

Why This Is a Board-Level Conversation in 2026
Three converging forces have elevated memory safety from an engineering concern to an executive one. First, in 2022, the NSA and CISA issued formal guidance recommending that organisations prioritise memory-safe languages for new development — a signal that governments now consider this a national-security-level concern, not just software quality.
Second, the Linux kernel’s adoption of Rust for device drivers (merged in Linux 6.1 in December 2022) established a meaningful precedent. If the kernel — long the domain of uncompromising C — can adopt a memory-safe language for new code, the argument that “C is necessary for performance” weakens considerably. This has sent a quiet message to the rest of the systems programming community.
Third, post-quantum cryptography migration — now underway following NIST’s PQC standard finalisation — is forcing organisations to audit and often rewrite cryptographic implementations. Memory safety bugs in cryptographic code are particularly dangerous, because they can leak keys, nonces, or plaintext through side channels or direct reads. That audit is giving many organisations their first honest look at the memory safety profile of their cryptographic dependencies.
“We now require that all new code be written in a memory safe language. Memory safety vulnerabilities are simply too costly to accept as a baseline risk.”— CISA, “The Case for Memory Safe Roadmaps” (2023)
For engineering leaders, the practical question is not “should we use Rust?” — most production systems cannot be rewritten wholesale. Instead, the question is: “given the language we are already in, what safety guarantees do we actually have, and where are our gaps?” The matrix above is a starting point for that conversation.
Choosing the Right Tool for the Risk Level
The right approach depends on what you are building and what your risk tolerance is. The table below maps typical use cases to the practical safety posture each language provides — along with the compensating controls you need if you stay in that language.
| Use Case | Language | Safety Posture | Key Compensating Controls |
|---|---|---|---|
| Systems / kernel code | C++ | High risk | AddressSanitizer, fuzz testing, static analysis, smart-ptr-only policy |
| Network services / APIs | Go | Low risk | Enable -race in CI, audit unsafe usage, CodeQL |
| Data pipelines / ML | Python | Medium risk | Vet C extension dependencies, use Dependabot, memory profiling |
| Enterprise / backend services | Java | Low-medium risk | Restrict Unsafe access, ZGC for latency-sensitive paths, static analysis |
| Cryptographic implementations | Any | High risk | Prefer audited libraries (BoringSSL, Bouncy Castle), formal verification where feasible |
Language Verdicts at a Glance
What We Have Learned
Memory safety is not a binary property — it is a spectrum, and every mainstream language occupies a different position on it. C++ gives maximum control but zero guarantees; its safety story depends entirely on engineering discipline and tooling. Go and Java sit in a practical sweet spot: their runtimes eliminate the most dangerous vulnerability classes by default, but both provide escape hatches — unsafe and Unsafe respectively — that reintroduce risk when performance demands it. Python is memory-safe at the application layer, but that safety is only as deep as its C extension ecosystem allows. What ties all four together is a common truth: every language has a seam where the managed world meets the unmanaged one, and that seam is where attackers look first. Knowing exactly where that seam is in your stack — and what controls you have around it — is the beginning of a mature security posture.




