This C program (primero.c) puts four numbers in a growable vector, takes a pointer to its first element, appends a fifth number and then prints the first one:

#include <stdio.h>
#include <stdlib.h>
 
typedef struct { int *datos; size_t len, cap; } Vector;
 
/* Appends x; if it doesn't fit, asks for a block twice as big. */
void push(Vector *v, int x) {
    if (v->len == v->cap) {
        v->cap = v->cap ? 2 * v->cap : 4;
        v->datos = realloc(v->datos, v->cap * sizeof *v->datos);
    }
    v->datos[v->len++] = x;
}
 
int main(void) {
    Vector v = {0};
    for (int i = 1; i <= 4; i++) push(&v, i);
 
    int *primero = &v.datos[0];
    push(&v, 5);
    printf("primero = %d\n", *primero);
}

(The examples were written for the Spanish edition of this post, so the names are in Spanish: datos means "data" and primero means "first".)

It compiles without a single warning, even with -Wall -Wextra, which switch on a good share of the compiler's warnings. It prints primero = 1, which is correct, with or without optimisation, and it would pass any test you threw at it. And yet it's wrong. Compile it with AddressSanitizer, a gcc and clang option that watches every memory access while the program runs, and you get this (trimmed):

ERROR: AddressSanitizer: heap-use-after-free on address 0x75f2c59e0010
READ of size 4 at 0x75f2c59e0010 thread T0
    #0 in main primero.c:21

Line 21 reads, through primero, a block that realloc has already freed. Printing a 1 is pure luck, and that luck depends on things that aren't in these twenty lines. The same operations in Rust take four lines, and they don't compile:

let mut v = vec![1, 2, 3, 4];
let primero = &v[0];
v.push(5);
println!("primero = {primero}");
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

In miniature, that is the difference between the two languages, and the reason much of the industry now writes in Rust what it would have written in C ten years ago. C leaves three facts to the programmer that the compiler never checks: how big a block of memory is, how long it lives and who touches it at the same time. When one of them goes wrong, the program doesn't stop. It carries on with the wrong data, or with somebody else's. Rust turns those three facts into rules the compiler checks before it produces a program. The price is a learning curve, slower builds and friction with the C that already exists, which is why the replacement is slow, partial and, in some places, never going to happen.

A pointer in C is a variable that holds a memory address: primero doesn't hold a 1, it holds the place where the 1 is. A program uses two areas of memory. The stack holds each function's local variables and is cleaned up automatically when the function returns. The heap holds the blocks the program requests while it runs, and in C the program has to free them itself. A language is memory-safe if a program written in it can't read or write outside the blocks it owns, or use a block after freeing it.

The example uses realloc, which resizes a block of memory that has already been allocated (usually to make it bigger) and keeps the data inside. If there's free memory right behind the block, realloc simply stretches it in place and returns the same address. If another block is in the way, it can't do that: it allocates a new block elsewhere, copies the data across, frees the old one and returns a different address. That's the danger in this program. primero still holds the old address, and nothing in C tells it that the address has stopped being valid.

Every result in this post was measured. Each example links to its program, and a script compiles and runs them all, so you can repeat any of them. The C compiler is gcc 15.2 with the C23 standard, and the Rust compiler is rustc 1.99.

Why C is everywhere

C was born at Bell Labs in 1972, when Dennis Ritchie created it to write Unix, and a year later Unix had been rewritten in it. It was a small language that sat one step above the machine: its types are the processor's types, an array is a memory address plus some arithmetic, and nothing happens unless the program explicitly asks for it. That's why people called C a "portable assembler". A C compiler was easy to write, so every new processor soon had one, and with it you could carry Unix, and everything built on top of it, to any machine.

Fifty years later, C is still underneath almost everything. The Linux kernel is written in C, as are large parts of the Windows and macOS kernels. So is each system's C standard library, which any program goes through to open a file or a connection. So are SQLite, Postgres, Redis, the Python interpreter, OpenSSL and curl, and the firmware in nearly every microcontroller inside a car, a washing machine or a pacemaker.

There's a second, less visible reason: C's ABI (application binary interface), the conventions that say how a compiled function receives its arguments and returns its result. Operating systems expose their services as C functions, and almost any language that wants to talk to another does so through them. Python calls C libraries, and so does Java, and a Rust program that opens a file on Linux ends up in a C function. C isn't just a language that software is written in. It's the common language software uses to talk to itself.

Timeline, not to scale, with ten milestones. In green, C and its standard: 1972, C is born, by Dennis Ritchie at Bell Labs; 1989, ANSI C, the first standard; 2011, C11, with threads and atomics; 2024, C23 is published. In orange, memory-safety failures: 1988, the Morris worm, which got in through a gets(); 2014, Heartbleed, in OpenSSL; 2019, Microsoft reports that 70% of its vulnerabilities are memory-safety bugs. In blue, Rust: 2015, Rust 1.0; 2022, Linux 6.1 accepts Rust code; 2025, Rust in Linux is no longer an experiment.

Not to scale. Memory-safety failures have dogged C from the start: the Morris worm, in 1988, got in through an unbounded read in a Unix server.

That's why replacing it is nothing like switching languages in a web application. Billions of lines of C are in production, much of it has been running for decades, and everything else talks to it.

What C leaves to the programmer

C was built on a philosophy that its own standards committee summed up for years in three words: trust the programmer. The language doesn't check what the programmer knows, and in practice that trust comes down to three facts.

How big a block of memory is. In C, an array passed to a function turns into a pointer to its first element, and the length is lost along the way. The function receives an address and has to take it on trust how much memory lies behind it. Nothing stops you from reading element one hundred of a four-element array: the program computes the address and reads whatever happens to be there.

How long it lives. A block obtained with malloc lives until someone calls free. The compiler doesn't know who is responsible for freeing it, or whether any pointers still point to it at that moment. Free it twice, or use it afterwards, and the program never notices.

Who modifies it at the same time. Two threads of execution can write to the same variable at the same moment. C11 added threads and atomic operations to the standard, but the compiler doesn't check which data each thread shares, or whether that data is protected.

Behind all three lies a fourth, subtler problem: undefined behaviour. The C standard lists the situations in which it doesn't say what should happen (the last public draft of C23 counts 221), among them reading outside an array, using a pointer to freed memory, two threads trampling on the same variable and a signed integer addition that exceeds the maximum value. That doesn't merely mean "the result is unpredictable". It means something stronger: the compiler is allowed to assume it never happens, and to optimise on that basis. If it does happen, the program has stopped meaning anything, and whatever it does, even working correctly, is a coincidence.

The C committee itself acknowledged in 2007, while preparing C11, that "trust the programmer" was an outdated principle where security is concerned, and wrote that into the charter that guides every revision of the standard. The numbers bear it out. In 2019 Microsoft reported that around 70% of the vulnerabilities it assigns a CVE each year (the public identifier under which a security flaw is registered) are memory-safety bugs, a share that had barely moved in twelve years. Chromium, the project Chrome is built from, reports the same figure for its serious security bugs, and half of those come from using memory after it has been freed. Android stood at 76% in 2019. All three figures cover C and C++ code together, and C++ shares C's memory model and, with it, the same bugs.

Five errors, in C and in Rust

The five examples below all follow the same order: the C program, what happens when it runs, the same program in Rust, and then either the Rust compiler's complaint or what the program does at run time. All five compile with gcc without a single warning.

Reading past the end

In April 2014 it emerged that Heartbleed, a bug in OpenSSL, the library that encrypted a large share of the internet's connections, had been sitting in released code for two years. The protocol included a heartbeat message: the client sends a few bytes and says how many it sent, and the server echoes them back to prove it's still alive. The server simply believed the length it was given. If the client sent four bytes and claimed they were 64 KB, the server sent back the four bytes plus almost 64 KB of its own memory, whatever happened to be there: passwords, session cookies, private keys.

This program (latido.c) reproduces the bug on a small scale. The names are Spanish again: latido is the heartbeat, mensaje the message, longitud its length and clave_del_servidor the server's key:

char *clave_del_servidor;   /* a secret the server keeps in memory */
 
/* Answers a heartbeat: returns the `longitud` bytes the client says it sent. */
char *responder(const char *mensaje, size_t longitud) {
    char *respuesta = malloc(longitud);
    memcpy(respuesta, mensaje, longitud);
    return respuesta;
}
 
int main(void) {
    char *mensaje = malloc(4);
    memcpy(mensaje, "HOLA", 4);                 /* the client sends 4 bytes */
 
    clave_del_servidor = malloc(32);
    strcpy(clave_del_servidor, "clave-privada-del-servidor");
 
    char *r = responder(mensaje, 64);           /* but says it sent 64 */
    for (int i = 0; i < 64; i++)
        putchar(r[i] >= 32 && r[i] < 127 ? r[i] : '.');
    putchar('\n');
}
HOLA....................1.......clave-privada-del-servidor......

The reply contains the message ("HOLA" is Spanish for "hello"), a few bytes malloc uses to keep track of its blocks (the dots and the 1) and, right after them, the server's private key, which sat in the next block. The result is identical at -O0, -O2 and -O3. AddressSanitizer catches it as a heap-buffer-overflow: a 64-byte read starting right at the end of a 4-byte block. But it only does so if the program was compiled with it, and production builds almost never are, because it slows the program down considerably.

In Rust, a reference to part of an array isn't just an address. It's the address and the length bundled together, and the language won't let you pull them apart. That pair is called a slice. The same function looks like this:

fn responder(mensaje: &[u8], longitud: usize) -> Vec<u8> {
    mensaje[..longitud].to_vec()
}

Run it with the same lie and you get:

thread 'main' (19168) panicked at latido.rs:3:12:
range end index 64 out of range for slice of length 4

The program stops instead of sending the key. A Rust panic is a controlled stop: the thread halts and releases what it was holding, without touching anyone else's memory. A server that a single malformed message can knock over isn't great either, so the idiomatic version asks for the slice with get, which returns "something or nothing" and makes you decide what to do in each case:

match mensaje.get(..longitud) {
    Some(trozo) => trozo.to_vec(),     // the length adds up
    None => return None,               // malformed heartbeat: dropped
}

What it costs. Checking the index on every access isn't free, but in practice it usually looks that way. The benchmark adds up 16 million numbers in two ways. Going through them in order, first to last, Rust takes the same time as C (10.2 ms against 10.0), because the compiler can see the index never exceeds the length and removes the check. Going through them in scattered order, with indices read from another array that the compiler can't predict, the check stays. With a small table that fits entirely in the processor's cache, adding with the check takes 16.0 ms and without it 12.8, which is 25% more. That's the worst possible case, a loop that does nothing but read, and even so the figure is fragile: an earlier, almost identical version of the same program gave 55%. With a table of 16 million numbers, which doesn't fit in the cache, all three versions take about the same time, around half a second, because waiting for memory costs far more than comparing two numbers. When the check really does matter, Rust lets you remove it with get_unchecked, but only inside a block marked unsafe, which comes up again below.

Using memory that has already been freed

Back to the program from the start. It prints a 1 because nothing sits behind the vector's block: realloc stretches it in place, and primero keeps pointing at the 1 by coincidence. All it takes is for the program to request memory for something else in between, and realloc has to move the block (primero_movido.c):

    for (int i = 1; i <= 4; i++) push(&v, i);
    char *nombre = strdup("serie");          /* another allocation, right behind the vector */
 
    int *primero = &v.datos[0];
    push(&v, 5);
    printf("%s: primero = %d\n", nombre, *primero);
serie: primero = 1430334168
serie: primero = -2021578024

A different number on every run: it's whatever malloc wrote into the block when it freed it. The bug was already in the original program, because as far as the C standard is concerned, the old pointer stops being valid after realloc even if the address hasn't changed. The new line, which has nothing to do with the vector, merely made it visible.

Three rows showing heap memory in the example primero.c. Row 1, before push(5): a block of four ints, 1, 2, 3 and 4; v.datos and primero point at its first cell. Row 2, if nothing is behind the block: realloc stretches it in place to eight cells, with the 5 in the fifth; v.datos and primero still point at the same 1, so the program prints 1 by luck. Row 3, if another allocation, the string serie, sits behind it: realloc gets a new block further on, copies the five ints and frees the old one; v.datos points at the new block, but primero still points at the old, freed one and reads garbage.

If nothing is in the way, realloc stretches the block and the old pointer stays valid, by luck. With another allocation behind it, realloc moves it, and the old pointer is left pointing at freed memory.

Reading garbage is the harmless version. If the freed block gets reused for something else, writing through the old pointer changes that other thing, and an attacker who manages to plant data of their choosing there can end up controlling the program. That's why these bugs, known as use-after-free, account for half of Chromium's serious memory-safety bugs.

Rust prevents it with two rules, which are the core idea of the language and the hardest part of learning it.

  1. Every value has a single owner. The vector belongs to the variable v. When v goes out of scope, the vector is freed automatically. There is no free to write, so there's no forgotten free and no free called twice. If v is assigned to another variable, the vector changes owner (it is moved), and using v afterwards is a compile error.
  2. A value can be lent out in one of two ways, and never both at once: to any number of readers (&v), or to a single writer (&mut v). And no loan can outlast the owner.

primero = &v[0] is a loan for reading. v.push(5) needs a loan for writing, and the second rule forbids that while primero is still in use. The compiler's full message points at the three lines involved:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> primero.rs:6:5
  |
5 |     let primero = &v[0];
  |                    - immutable borrow occurs here
6 |     v.push(5);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |     println!("primero = {primero}");
  |                          ------- immutable borrow later used here

The part of the compiler that enforces these rules is called the borrow checker. It knows nothing about realloc, or about push possibly moving the block. All it knows is that push wants to modify the vector, and that nobody may modify a vector while someone else is reading it. Applied without exceptions, that rule covers the realloc case and every case like it. The fix, in Rust as in C, is to not hold on to the address: keep the index (let primero = 0;, then read v[primero] later) or copy the value (let primero = v[0];).

NULL

In C, a function that searches for something and might not find it usually returns NULL, the pointer that points to nothing, and the caller has to remember to check (buscar.c; buscar means "find", clase is a class, alumno a student and nota their mark):

typedef struct { const char *nombre; int nota; } Alumno;
 
/* Returns the student with that name, or NULL if there isn't one. */
Alumno *buscar(Alumno *clase, size_t n, const char *nombre) {
    for (size_t i = 0; i < n; i++)
        if (strcmp(clase[i].nombre, nombre) == 0) return &clase[i];
    return NULL;
}
 
int main(void) {
    Alumno clase[] = {{"Ana", 9}, {"Luis", 6}, {"Sara", 8}};
    printf("Sara: %d\n", buscar(clase, 3, "Sara")->nota);
    printf("Marta: %d\n", buscar(clase, 3, "Marta")->nota);
}
Sara: 8
Segmentation fault (core dumped)

In an ordinary program, following a NULL ends like this, with the operating system killing the program, and that's the best case. Tony Hoare, who invented the null reference for the ALGOL W language in 1965, called it a "billion-dollar mistake" in 2009.

Rust has no NULL. A function that might not find anything says so in its type: it returns Option<&Alumno>, which is either Some(alumno) or None.

fn buscar<'a>(clase: &'a [Alumno], nombre: &str) -> Option<&'a Alumno> {
    clase.iter().find(|a| a.nombre == nombre)
}

(The 'a says that the student it returns is borrowed from clase and can't outlive it.) If you use the result the way you would in C, without checking whether there's a student, it doesn't compile:

error[E0609]: no field `nota` on type `Option<&Alumno>`

The compiler suggests .unwrap(), which means "I know there's a student here, and if there isn't, stop the program". You're free to write it, but it's right there in the code, where anyone reading can see exactly where someone chose to take the risk. The normal approach is to handle both cases with match. And it costs nothing: an Option<&Alumno> takes 8 bytes in Rust, the same as an &Alumno. The compiler represents None as address zero, which a valid reference can never have, so the compiled program compares against zero exactly as the C one does. The difference is that in Rust that comparison can't be forgotten.

Two threads, one counter

Four threads each add a million to the same variable (contador.c; contador is the counter, sumar means "add" and hilos are the threads). The result should be four million:

#include <pthread.h>
#include <stdio.h>
 
long contador = 0;
 
void *sumar(void *arg) {
    (void)arg;
    for (int i = 0; i < 1000000; i++) contador++;
    return NULL;
}
 
int main(void) {
    pthread_t hilos[4];
    for (int i = 0; i < 4; i++) pthread_create(&hilos[i], NULL, sumar, NULL);
    for (int i = 0; i < 4; i++) pthread_join(hilos[i], NULL);
    printf("contador = %ld\n", contador);
}

Compiled without optimisation, every run prints a different number, somewhere between 1 and 2.3 million on the test laptop. contador++ is really three steps (read, add, write), and when two threads interleave them, an increment gets lost. That's called a data race.

Compiled with -O2, the result is always 4,000,000, though not because the bug has gone away. The optimiser has turned the whole loop into a single instruction that adds a million in one go:

sumar:
    addq    $1000000, contador(%rip)

It's allowed to, because a data race is undefined behaviour: the compiler is entitled to assume that no other thread touches contador while the loop runs. The window in which two threads can trip over each other has shrunk from a million iterations to a single instruction, and the bug has gone into hiding. It will resurface the day the loop does something more than add. ThreadSanitizer, the thread-oriented counterpart of AddressSanitizer, catches it: WARNING: ThreadSanitizer: data race.

In Rust, the same program doesn't compile, and for the same reason as before. Every thread needs to modify contador, and only one loan for writing can exist at a time. Here is the code and the compiler's message, trimmed:

let mut contador: u64 = 0;
thread::scope(|s| {
    for _ in 0..4 {
        s.spawn(|| {
            for _ in 0..1_000_000 { contador += 1; }
        });
    }
});
error[E0499]: cannot borrow `contador` as mutable more than once at a time
 --> contador.rs:8:21
  |
8 |             s.spawn(|| {
  |                     ^^ `contador` was mutably borrowed here in the previous iteration of the loop
9 |                 for _ in 0..1_000_000 { contador += 1; }
  |                                         -------- borrows occur due to use of `contador` in closure

The rule that prevented the use-after-free prevents the data race too, without a single extra line. To share something between threads you have to use a type designed to be shared: an atomic integer (AtomicU64, whose fetch_add adds in one indivisible step, so no other thread can slip in between) or a lock (Mutex). With the atomic, the corrected version prints 4,000,000 on every run. Rust takes the check one step further: every type declares whether it can be sent to another thread and whether it can be shared between threads, and the compiler verifies both. A non-atomic reference counter, for instance, can't be sent to another thread, and the error shows up at compile time, not three days into production.

The check the compiler deletes

The last example has no pointers. The function cabe ("it fits") wants to know whether y can be added to x without exceeding the maximum value of an int (cabe.c), and it checks in the most natural way: if the addition overflows, the result wraps around and ends up smaller than x.

/* Can y (positive) be added to x without going past the maximum of an int? */
int cabe(int x, int y) {
    return x + y > x;
}

With x = INT_MAX and y = 1 it should return 0. It returns 1, with or without optimisation. The assembly gcc generates without optimising doesn't even look at x:

cabe:
    ...
    cmpl    $0, -8(%rbp)      # compares y with 0
    setg    %al               # returns y > 0

As far as the compiler is concerned, a signed integer addition that overflows is undefined behaviour, so it may assume that never happens. And if it never happens, x + y > x is the same as y > 0: plain school algebra. The overflow check vanished precisely because it was checking for an overflow. Compiled with -fsanitize=undefined, gcc warns at run time (signed integer overflow: 2147483647 + 1 cannot be represented in type 'int') and returns 0.

This isn't a textbook curiosity. In 2009, the Linux kernel had a security hole in a network driver because a function used a pointer and, one line later, checked whether it was NULL. Since using the pointer first implied it couldn't be null, gcc deleted the check. Today Linux is built with options that forbid the compiler from drawing such conclusions: one for null pointers, added in the wake of that bug, and another for overflow. In other words, the kernel is compiled as a dialect of C that differs slightly from the standard one.

In Rust, an addition that overflows isn't undefined behaviour. Compiled without optimisation, the program stops (attempt to add with overflow); compiled with optimisation, the addition wraps around, as it does in the processor, and cabe returns false, which is correct. And to ask what the function really wanted to ask, there's an operation that says it outright: x.checked_add(y) returns None if the sum doesn't fit.

And the everyday annoyances

The five examples are about safety, but safety isn't the only thing that wears you down in C. Rust also does things differently in other areas, and those differences start to weigh on you as a project grows:

In CIn Rust
errorsa return code (-1) and the global variable errno, easy to ignoreResult<T, E>, which you have to look at; ? propagates it in one character
generic codepreprocessor macros or untyped void * pointersgenerics and traits, checked by the compiler
types with variantsa struct with a tag and a union, by handenum with data, and match warns if a case is missing
stringsbytes ending in a zero, so measuring one means walking through itstored length and valid UTF-8 text
dependencies and builds.h headers, Make or CMake, no package managermodules and Cargo, which builds, tests and fetches dependencies

None of these differences would justify switching languages on its own. Together, they explain why people who move to Rust for the safety tend to stay for everything else.

What Rust doesn't fix

Rust doesn't make every problem above impossible. What it promises is more precise: a program that doesn't use unsafe can't have undefined behaviour.

unsafe is a marker that, inside a block, unlocks five things the compiler can't check: reading through a raw pointer (one with no borrowing rules, like C's), calling functions marked unsafe (every C function among them), touching mutable global variables, and two more of lesser importance. You need it to talk to hardware, call C code and write certain data structures. Even the standard library's Vec uses unsafe internally to manage its block of memory. The idea is to keep unsafe confined to small, carefully reviewed pieces that present a safe interface to the outside world. Whether those pieces really uphold the rules isn't a matter of faith: the RustBelt project formally proved, on a model of the language, that Rust's rules guarantee what they promise, even for libraries such as Arc and Mutex that use unsafe internally. In Android, about 4% of the Rust code sits inside unsafe blocks.

And that 4% is where the bugs have turned up. In 2025, Google came close to shipping its first memory-safety bug in Android's Rust code, an overflow in an AVIF image decoder, inside unsafe code. It caught the bug before release, and Android's memory allocator would have blocked any exploit anyway. In December 2025 came the first CVE in Linux's Rust code. In the binder driver, which Android uses for communication between processes, an unsafe block removed items from a list on the assumption that no other thread was touching it at the same time, and another thread was. It could hang the system, but not take control of it.

Outside memory safety, Rust makes no promises, and several kinds of problems still get through:

  • Memory leaks. As far as Rust is concerned, never freeing memory is safe, because it isn't undefined behaviour. Two values that point to each other through reference counters are never freed.
  • Crashes. A panic in a server is a denial of service, like the heartbeat example: the program doesn't leak the key, but it stops answering.
  • Deadlocks. Two threads that each wait for the other's lock will wait forever, in Rust just as in C. Rust prevents data races, not things happening in the wrong order.
  • Logic errors. The compiler checks that the program doesn't misuse memory, not that it does what it's supposed to do. There's an example further down.
  • Dependencies. Cargo makes adding libraries so easy that a mid-sized project can depend on hundreds of them, written by people nobody has vetted.

Who is adopting it, and how

The best-documented case is Android, and its main lesson isn't the one you'd expect. Around 2019, Google started writing Android's new code in memory-safe languages: Java and Kotlin where possible, and Rust in the system components that used to be written in C and C++. It did not rewrite the old code. Even so, the share of Android's vulnerabilities that are memory-safety bugs fell like this:

Bar chart. Share of Android's vulnerabilities that are memory-safety bugs, according to Google's security blog: 76% in 2019, 35% in 2022, 24% in 2024 (estimated for the full year) and below 20% in 2025. A dashed line marks 70%, the industry norm according to Google. 2020, 2021 and 2023 have no bar because Google does not give the figure in the text.

Source: Google's security blog, posts from December 2022, September 2024 and November 2025. Covers Android's C and C++ code.

The explanation is that vulnerabilities age. Almost all of them sit in code that was written or changed recently, and as code gets used, tested and fixed, fewer remain. According to Google's data, five-year-old code has between 3.4 and 7.4 times fewer vulnerabilities than newly written code. Stop adding new C, and the C that's left grows older and its bugs run out on their own. Rewriting the old code, the expensive part, is what pays off least.

Google has published two more figures worth quoting with care. The first is bug density: roughly a thousand memory-safety bugs per million lines in Android's historical C and C++, against 0.2 in its five million lines of Rust. That 0.2 rests on a single bug, the AVIF one, so it's a very rough estimate, but the gap is three orders of magnitude. The second was more of a surprise: medium-sized and large changes in Rust are rolled back about a quarter as often as C++ ones, and spend around 25% less time in review. In Android, the safe code is also the code that ships sooner.

Rust entered the Linux kernel in version 6.1, in December 2022, as an experiment. At the annual maintainers' summit in December 2025, the experiment was declared a success: Rust is here to stay. Drivers written in Rust already exist, such as Android's binder, and more are on the way, such as Nova, for NVIDIA graphics cards. The maintainer of the graphics subsystem estimated at the time that within a year they would stop accepting new graphics drivers written in C. Ubuntu 25.10, released in October 2025, ships Rust versions of sudo and the basic command-line utilities (ls, cp, date and the rest) by default. Git has announced that Rust will be required to build it from version 3.0.

Governments are pushing, too. In December 2023, the cybersecurity agencies of the United States (CISA and the NSA) and four other countries published a guide asking software makers for a plan to move away from languages that aren't memory-safe. In October 2024, CISA and the FBI went further and asked makers of products already written in those languages to publish that plan by 1 January 2026. These are recommendations, not laws, and they don't name a language: Java, C#, Go and Swift are memory-safe too. But they all manage memory on their own while the program runs: Java, C# and Go with a garbage collector, which pauses the program or consumes processor time whenever it needs to, and Swift by counting references on every assignment. In an operating system kernel, a driver or a microcontroller, that's unacceptable. Rust is the most mature and widely used of the few languages that offer memory safety without any of that, which is why it's taking C's place, not Java's.

What switching costs

Rewriting is risky. Old code may be ugly, but it carries decades of fixes for odd cases nobody documented. In October 2025, shortly after its release, Ubuntu 25.10 stopped installing automatic updates. The cause was the new Rust version of date, which accepted the -r option (it should print a file's modification date) but ignored it and printed the current date instead. The program that decides whether it's time to check for updates compares the age of a file, so it always got zero and never concluded that it was time. That's a logic error the Rust compiler couldn't see, and exactly the kind of bug any rewrite brings, in any language.

The boundary between C and Rust takes work. A Rust program calls C by declaring the functions with extern "C", and every such call is unsafe, because the compiler can't check what happens on the other side. Tools can generate those declarations from the C headers, but the important things aren't in the headers: who frees each block, how long each pointer lives, which functions can be called from several threads. Someone has to read the C code, understand it and translate it into Rust types. In Linux, that sparked bitter public arguments between maintainers over who fixes the Rust side when a C interface changes, and some people left the project because of them.

Two compilers instead of one. The project now depends on two toolchains, each with its own versions, and not every processor has both. The Rust compiler uses LLVM to generate code, and gcc reaches architectures LLVM doesn't. When the maintainer of APT, Debian's package manager, announced plans in 2025 to make Rust mandatory, the first to be affected were the Debian ports for old processors (m68k, Alpha, PA-RISC, SuperH), which had no decent Rust compiler. Two projects aim to make gcc compile Rust, and neither is finished.

Certification is expensive. In aircraft, cars and medical devices, the compiler itself has to be certified against standards such as ISO 26262. C has had certified compilers for decades. The first one for Rust, Ferrocene, arrived in 2023. Certifying it required a specification of the language, which the same company wrote and then donated to the Rust project in 2025, since the project didn't have one.

Learning takes time, though less than people claim. In an internal Google survey of more than a thousand developers who wrote Rust in 2022, over two thirds felt they could contribute to a Rust codebase within two months or less. A third were as productive as in their previous language in that time, and more than half within four months.

Rust's drawbacks compared with C

On top of what the switch costs, Rust has drawbacks of its own, which would still be there in a project started from scratch.

The borrow checker rejects correct programs. It's conservative: if it can't prove that a program is safe, it rejects it, even when it is. Structures in which many parts point at one another (graphs, doubly linked lists, trees whose nodes know their parent) collide head-on with the single-owner rule. In C, a doubly linked list is twenty lines. In Rust, there's an entire book about writing linked lists, and the doubly linked one ends up using unsafe or reference counters. The usual way out is a different way of thinking: keep the nodes in a vector and have them refer to each other by index. It works, but you have to learn it.

It compiles slowly. With a five-line program you won't notice: gcc takes 0.10 seconds and rustc 0.15. With a large project you will. Rust generates a copy of each generic function's code for every type it's used with, the borrow checker has work to do, and dependencies are compiled from source. In Google's survey, slow builds were by far the number one complaint, and they still top the list of productivity problems in the Rust project's own annual survey.

Executables are bigger. The program that prints "hola" ("hello") takes 15,952 bytes in C and 4,536,368 in Rust, which drops to 336,744 once the debugging information is stripped. The difference is that the C program uses the standard library already installed on the system, while the Rust one carries its own. On a server that doesn't matter. On a microcontroller with 64 KB of memory it does, which is why Rust lets you do without the standard library, at the price of giving up much of what it provides.

It has no stable ABI. The way Rust lays out data and passes arguments can change between compiler versions. A Rust library that other languages need to call, or that Rust code built with a different compiler version needs to call, must expose functions with the C convention (extern "C") and data laid out as in C (#[repr(C)]). To talk to the world, Rust speaks C too.

There's only one real compiler. C has gcc, clang, Microsoft's compiler and dozens of compilers for microcontrollers, and an ISO standard that says what they must do. Rust has rustc, and its official specification dates from 2025. For most projects that doesn't matter. For those that need two independent implementations, or a compiler for an unusual processor, it does.

Anyone who wants something closer to C, without the borrow checker, can look at Zig: a small language that compiles C and mixes with it seamlessly, checks array indices in its safe build modes and has no implicit NULL, but doesn't prevent use-after-free or data races. Projects such as Bun, TigerBeetle and Ghostty use it, although as of 2026 it still hasn't reached version 1.0, and each new release tends to break code written for the previous one.

When to choose each

The useful question isn't "C or Rust?" in general, but for which code.

situationchoicewhy
code that reads untrusted input: network protocols, file formats, parsersRustit's where almost all the vulnerabilities are
a new project that will live for years and have many authorsRustthe compiler keeps track of what nobody will remember in five years
heavy concurrencyRustdata races are compile errors
a large project already in CC, with Rust for new coderewriting the old code is what pays off least
a microcontroller whose vendor only provides a C compilerCthere's no realistic alternative
a library that many languages will calla C interfacethe implementation can be Rust
learning how memory worksCRust deliberately hides what you need to see

Sometimes the answer is neither. If a garbage collector is acceptable, as it is in almost any network service or command-line tool, Go is simpler than both, and it's what Docker and Kubernetes are written in.

And if you have to stay with C, you can write considerably safer C: compile with -Wall -Wextra and treat warnings as errors, run the tests with -fsanitize=address,undefined (plus thread if there are threads), test with randomly generated inputs (fuzzing), and build what you ship with the compiler's hardening options (-fhardened in gcc 14 or later). None of these turns C into a safe language, but at least one of them would have caught each example in this post.

How C is evolving

C isn't standing still. It changes slowly, and on purpose: much of its value lies in forty-year-old code still compiling and in there being a compiler for every processor, so every new feature has to win over the people who maintain dozens of compilers.

The standard's first serious attempt to tackle buffer overflows went badly. C11 added an optional annex with versions of the string functions that take the size of the destination (strcpy_s, memcpy_s and the like). Four years later, a report to the committee itself found that hardly anyone had implemented them, that Linux's C library had turned them down several times and that code using them came out no safer, and it proposed removing them.

C23, the standard published in 2024, is the biggest revision since 1999, and several of its new features target problems covered in this post: nullptr, a null pointer with a type of its own; [[nodiscard]], which makes the compiler warn when a function's return value is ignored; two's complement as the mandatory representation for signed integers (although overflowing them is still undefined behaviour); and <stdckdint.h>, with addition, subtraction and multiplication functions that tell you whether the result overflowed. With it, the example function comes out right:

#include <stdckdint.h>
 
int cabe(int x, int y) {
    int suma;
    return !ckd_add(&suma, x, y);     /* ckd_add returns true if it overflowed */
}

Called with INT_MAX and 1, it returns 0, with or without optimisation. The next revision, which the committee calls C2y until it gets a year, already includes _Countof. It gives the number of elements in an array without the old trick of dividing one sizeof by another, which fails silently once the array has turned into a pointer. And defer, which lets a function release its resources on the way out without repeating the free before every return, is advancing as a separate technical specification, a document that compilers can implement before it makes it into the standard.

Outside the standard, things move faster. Compilers keep adding protections: -fhardened switches on, in one go, checks on string functions, stack protection and several more. Fil-C is a C compiler that makes ordinary C memory-safe with almost no changes to the code, using a garbage collector and a check on every access, at the cost of a slower program (between one and four times slower, according to published measurements). And hardware has started to help. ARM's memory tagging (MTE), which Pixel phones have offered since the Pixel 8 and which Apple turns on in the iPhone 17, tags each block and each pointer and halts the program when the tags don't match. Processors with CHERI, still mostly experimental, turn every pointer into a capability that carries its own bounds.

All these measures have something in common: they catch the error while the program is running, or make it harder to exploit. None of them prevents it at compile time the way the borrow checker does. And C can't add a check like that without breaking almost all the C that exists, because almost all of it shares and frees memory in ways such rules forbid. A C with borrowing would be a different language.

Will C disappear?

There is a precedent. C++ was born in the early eighties as a better C, compatible with it, with the explicit aim of replacing it. Forty years later, C is still here, and C++ shares its memory problems. Fortran and COBOL, older than C, still run in banks and computing centres, and both got a new standard in 2023. Languages with a lot of code behind them don't die; people just stop starting new projects in them.

And C has three reasons to stay that those languages lacked.

It's the common language. As long as operating systems expose their services as C functions, and languages call each other through the C ABI, C will go on existing even if nobody writes a whole program in it. To talk to the world, Rust speaks C.

The code that already exists isn't worth rewriting. That's Android's lesson: old code, left alone, gets safer with the years. What pays off is writing the new code in another language, which leaves billions of lines of C running for decades, and someone has to maintain them.

There are places Rust doesn't reach. 8-bit microcontrollers, signal processors that only have the vendor's compiler, certified environments where changing compilers takes years. There, C isn't competing with Rust. It's competing with assembly.

What will change is what gets written in C. The code that reads untrusted input, which produces the most vulnerabilities, is moving to Rust, pushed by Google, Microsoft, Linux and the regulators. C is on its way to taking assembly's place: it never went away and it's still underneath everything, but fewer people write it, for specific tasks, and knowing why.

And C is evolving, just not in order to stop being C. The new features in C23 and C2y sand down rough edges, the tools catch more errors and the hardware makes them harder to exploit. None of that gives C what Rust has, because what Rust has is exactly what C chose not to have: a compiler that doesn't trust the programmer.

What to take away

If you take away only one thing, make it this: C's memory-safety bugs aren't the slips of bad programmers. They come from a language that leaves the programmer in charge of how big each block is, how long it lives and who touches it at the same time, and from a compiler that assumes the programmer got it right. Rust doesn't make programmers better. It makes the compiler check those three facts, and the price is learning to write programs the compiler can check.

And if you can take away three more things, make them practical ones.

If you write C, run your tests with the sanitizers. -fsanitize=address,undefined would have found four of the five errors in this post, and -fsanitize=thread the fifth. Each costs one line in your test build.

Write new code that reads untrusted input in Rust. A parser, a protocol, a file format. That's where the exploitable bugs are, and where Rust makes the biggest difference.

Don't rewrite what works just for the sake of rewriting it. Old C code, left alone, has fewer bugs every year. Write the new code in Rust, take care of the boundary between the two, and let the C age.

C has spent fifty years trusting the programmer, and for most of that time it was the only way to write fast systems software. That's no longer true. What won't go away is the need to understand what C teaches: what an address is, how long a block of memory lives, what happens when two threads touch the same thing. Rust doesn't make any of that matter less. It just has the compiler check it for you.