DEV Community

Cover image for How Memory Safety CVEs Differ Between Rust and C/C++
Juan Torchia
Juan Torchia Subscriber

Posted on • Originally published at juanchi.dev

How Memory Safety CVEs Differ Between Rust and C/C++

How Memory Safety CVEs Differ Between Rust and C/C++

Why do we keep measuring language security by CVE count when we know that number depends as much on installed base size as on any actual property of the language? It took years of debate and a couple of NSA and CISA papers for the ecosystem to take the question seriously — and even then, the answer circulating in most threads is too simple to be useful.

Here's my thesis: the difference in memory safety CVEs between Rust and C/C++ is real, documentable, and technically interesting. But turning it into "migrate everything to Rust" or "the borrow checker solves it all" is a category error. The useful data isn't in the headline — it's in which vulnerabilities disappear, which ones persist, and under what conditions Rust's security model has its own friction.


The real problem: not all memory CVEs are the same

When CISA, NSA, or the White House Office of the National Cyber Director publish reports recommending memory-safe languages (and they did, publicly, between 2022 and 2023), the category they're targeting is specific: vulnerabilities caused by undefined behavior in manual memory management. Use-after-free, buffer overflow, double-free, unchecked null pointer dereference — the classic C/C++ family.

The technical distinction matters:

Vulnerability class C/C++ Rust (safe) Rust (unsafe)
Use-after-free Common Impossible by design Possible
Buffer overflow (stack/heap) Common Impossible by design Possible
Data race in multithreading Common Impossible by design Possible
Integer overflow Possible Debug: panic / Release: wrapping Same
Logic bugs Always possible Always possible Always possible
Misused unsafe block N/A Possible Possible

This table isn't a production benchmark — it's a map of what guarantees the Rust compiler gives you in safe code vs. what falls outside those guarantees. This isn't my own claim: the ownership model and borrow checker rules are formally described in The Rust Reference and in the unsafe chapter of the official book.

The point most ignored in Twitter/HN discussions: roughly 70% of the code in a typical Rust project can live in safe, but any C integration via FFI, any unsafe block for low-level operations, and any crate dependency that uses unsafe internally falls right back into C territory. This isn't a hypothetical — libs like tokio, serde, and ring have reviewed and audited unsafe blocks, but the compiler's security contract doesn't apply there.


Available evidence: what you can verify without access to someone else's production

You don't need a proprietary CVE dataset to validate something concrete. There are at least three reproducible public sources:

1. RustSec Advisory Database
The rustsec/advisory-db repository on GitHub maintains security advisories for the Rust ecosystem. It's auditable, has categories by vulnerability type and date. You can run:

# Install cargo-audit if you don't have it
cargo install cargo-audit

# Audit dependencies of any Rust project
cargo audit

# View active advisories with detail
cargo audit --json | jq '.vulnerabilities.list[] | {id: .advisory.id, title: .advisory.title, categories: .advisory.categories}'
Enter fullscreen mode Exit fullscreen mode

The output tells you not just whether there are known vulnerabilities, but what category they fall into. That data is concrete and reproducible on any machine.

2. CVE Details / NVD by CWE
The NVD (National Vulnerability Database) categorizes CVEs by CWE (Common Weakness Enumeration). CWE-119 (buffer errors), CWE-416 (use-after-free), and CWE-476 (null pointer dereference) are the categories that concentrate the bulk of historical CVEs in C/C++ projects. You can filter by language and year at nvd.nist.gov and observe the distribution. The volume is asymmetric — and part of that asymmetry is installed base, not just language safety.

3. Chromium and Microsoft as public reference cases
Google published internal data showing that ~70% of severe Chrome CVEs over a given period were memory safety issues in C++. Microsoft did the equivalent for their products. Those numbers get cited constantly, but context matters: these are C++ projects with tens of millions of lines and decades of technical debt. They're not representative of a new, well-audited C++ project.


Where people get it wrong: the too-fast recipe

The most common mistake I see in technical discussions is treating the CVE comparison as a direct adoption argument. The reasoning usually goes:

"Rust has fewer memory CVEs → migrate the stack → problem solved."

There are three costs that recipe hides:

Cost 1: The invisible unsafe in dependencies. If you pull in a crates.io dependency without checking its advisories, you're potentially importing unaudited unsafe. cargo audit catches it if there's a registered advisory — but there's unsafe in crates without advisories because nobody's audited them yet. The borrow checker can't protect you from what it can't see.

Cost 2: The unsafe curve in FFI. If the system you want to protect does FFI with C (drivers, hardware libraries, bindings to OpenSSL/libsodium), that interface is unsafe territory. A poorly written wrapper can introduce exactly the same bugs you were trying to avoid. Rust's guarantee ends at the boundary of the unsafe block.

Cost 3: Logic bugs and business logic vulnerabilities. The borrow checker solves a specific class of bugs — memory management ones. A TOCTOU, a race condition in business logic, bad input validation, a broken permissions schema: none of those disappear by switching languages. This isn't an argument against Rust — it's an argument against believing the language holistically solves security.

An honest read of this connects to something I've learned more than once looking at validation schemas: the place that hurts most isn't the one the compiler protects, it's the one you assume is covered and isn't. Same as when you write a Zod schema once and expect it to validate the entire flow — but there are three ways it breaks at runtime that aren't obvious until you see them.


Decision matrix: when this data changes something and when it doesn't

Before using the CVE comparison as a technical argument, run it through this checklist:

CHECKLIST: Is the Rust vs C/C++ CVE data relevant to my decision?

[ ] Does the system I'm evaluating have C/C++ with manual memory management?
    → If not, the comparison is irrelevant to you.

[ ] Is the primary attack surface memory vulnerabilities (UAF, overflow)?
    → If the dominant risk is business logic or authentication, Rust doesn't change that.

[ ] Do I have the capacity to review unsafe blocks in critical dependencies?
    → If not, the gain shrinks: you're importing opaque risk just like in C.

[ ] Does the project use extensive FFI with C?
    → The borrow checker's benefit is scoped to the safe portion of the code.

[ ] Am I evaluating new code vs. migrating existing code?
    → Migrating a large C/C++ codebase has real rewrite costs and a coexistence period.
    → New code in Rust on a well-scoped domain: the benefit is immediate and verifiable.

[ ] Does the team have experience with the ownership model?
    → The borrow checker learning curve is real. A team without Rust experience
      can introduce more bugs during the transition than it avoids in the medium term.
Enter fullscreen mode Exit fullscreen mode

For projects where the domain is low-level computation, parsing untrusted binary formats, system daemons, or network components with high exposure, the argument for Rust is solid and backed by public evidence. For a business backend in TypeScript or Java where the dominant risk is injection, poorly implemented authentication, or broken permissions logic, the memory safety debate is almost a distraction.

This connects to something that also applies to the authentication token decision tree: the right technical choice depends on the actual threat model, not on whichever language or protocol has the best security marketing.


FAQ

Does Rust eliminate all memory CVEs?
No. It eliminates memory CVEs in safe code — which is the majority of code in a well-structured project. Any unsafe block, FFI with C, or external crate with unaudited unsafe falls outside that guarantee. The borrow checker is a contract with the compiler, not a global security scanner.

Do Rust projects have zero memory safety CVEs?
Not exactly. The public rustsec/advisory-db database has advisories for Rust projects, some with memory safety categories originating in unsafe blocks or in crates with bugs prior to an audit. They're fewer in absolute and proportional terms than in equivalent C/C++ projects, but "fewer" isn't "zero."

Does it make sense to migrate a TypeScript/Node backend to Rust for security?
In most cases, no. The threat model of a business backend isn't dominated by memory management vulnerabilities — it's dominated by authentication logic, input validation, and infrastructure configuration. Rust doesn't change that. The decision makes more sense for parsing components, cryptography, or low-level networking.

What's the practical difference between a C/C++ CVE and a Rust one?
Memory CVEs in C/C++ are typically directly exploitable: a buffer overflow can lead to arbitrary code execution. CVEs in Rust tend to be more contained — panic, memory leak, or incorrect behavior in edge cases — and less frequently escalate to arbitrary execution. That difference in average severity is technically significant even if the raw count is lower.

Is cargo audit enough to audit a Rust project?
It's a good first step, but it's not complete. It covers advisories registered in rustsec/advisory-db. It doesn't detect unaudited unsafe without an advisory, logic bugs, or vulnerabilities in system dependencies (dynamically linked C libraries). For a serious audit, you complement it with cargo-geiger (which counts and identifies unsafe across the dependency tree) and manual review of critical dependencies.

Does any of this change things for someone working primarily with TypeScript/Next.js?
Directly, not much. The relevant security model for that stack is different: schema validation, session management, HTTP headers, database permissions. Understanding the Rust/C++ comparison is useful for making architecture decisions when there are low-level components involved, or for evaluating whether a native dependency (a Node.js addon in C++) is worth the risk. It's not a day-to-day decision in a standard Next.js project.


Closing: what the data says and what it can't say

The difference in memory safety CVEs between Rust and C/C++ is a real phenomenon, documented in public sources and technically explainable by the compiler's ownership model. It's not hype — there's a structural reason why a certain class of bugs is impossible in Rust safe code.

The uncomfortable part is that this data gets used constantly to justify decisions that are badly framed. If a system's threat model isn't dominated by manual memory management, the CVE comparison doesn't resolve anything. If the team doesn't have the capacity to review unsafe in dependencies, the compiler's guarantee dilutes in practice.

My position after looking at this from multiple angles: Rust is a solid tool for specific domains where memory management is the primary risk. As a universal security argument, it falls short. The practical step I'd recommend before any decision: run cargo audit and cargo geiger on any Rust project you're considering adopting, look at what percentage of the dependency tree has unsafe, and decide with that number in hand — not with the headline.

The same applies to architecture decisions in general: formal methods have a clear ceiling, and believing a tool holistically solves security is the most elegant way to drop your guard exactly where it matters most.


This article was originally published on juanchi.dev

Top comments (0)