Skip to content

C and C++ — the bugs Rust is a reply to

Level: 201 · for C and C++ programmers

One line: Every compile-time guarantee on Google's benefits list is a reply to a specific C or C++ bug, and the list is far more convincing once you have watched the bug happen.

Benefits of Rust sorts twenty claims by the evidence behind each one. This section supplies the other half: for each claim, the program the claim is about — compiled with the C compiler already on your machine, run, and its real output printed. Nine short programs, nine failures, and the Rust that will not build them.

The bug Kind What it did here Rust's reply
Uninitialized reads initialization the same stale number every run at -O0, a different one every run at -O2 E0381 — the read, not the declaration, is what is checked
Double-free temporal SIGABRT, "Double free of object" one value, one owner, so one free
Use-after-free temporal printed 6382657, which is the string "Ada" a later malloc put there a reference may not outlive what it points at
Null dereference null SIGSEGV, no output at all absence is Option, a type you must open
Forgotten unlock none — a deadlock printed two lines and then hung forever the guard is a value; dropping it unlocks
Data races data race counted to 1,149,618 instead of 2,000,000 — and to exactly 2,000,000 once optimized Send, Sync, and a borrow checker that spans threads
Iterator invalidation temporal silently kept one of the two elements it was asked to delete E0502 — you cannot read and mutate in one breath
Buffer overruns spatial summed three small numbers to 1433338037, differently each run every index checked; .get() when you would rather ask
Signed overflow integer overflow x + 1 > x was false at -O0 and true at -O2 overflow is defined, and you name which definition

Kind is the taxonomy the C++Now 2026 talk below opens with: spatial, temporal, type, initialization and data-race safety, with null and integer-overflow safety as goals of their own. None of the nine is a type confusion — the one class this section does not show yet.

Chromium's security team puts numbers on why this matters: around 70% of their high-severity security bugs are memory-unsafety problems, and half of those are use-after-free — measured across 912 high or critical bugs since 2015 ↗.

What C++ is doing about it

The nine pages show what C and C++ allow. The next two show C++'s own replies — the plan Yitzhak Mandelbaum laid out for Google at C++Now 2026 in A Path to Practically Safe C++ — each measured on Clang 21 and 23, beside the Rust it resembles.

The reply Kind What it adds to C++ How close it gets to Rust
Safe Buffers spatial a length on every buffer, a check on every index, raw-pointer indexing flagged the same three rules — as a warning and a build mode rather than a default
Lifetime safety in Clang temporal a flow-sensitive check that no pointer is read after its object died the borrow checker's first rule, down to the three-point diagnostic — without its second

Why these pages have no answer key

Every other page in this library ends with a transcript that CI recompiles, re-runs and diffs against a recorded file. The C halves of these pages cannot work that way, and the reason is the subject:

Undefined behaviour — a program the language standard declines to define at all, so no output is the "right" one. See the glossary.

A recorded answer key is a claim that a program has one correct output. All nine programs here are undefined behaviour, so none of them has one. Several are reproducible anyway — the double-free aborts every single time — but that is a fact about this allocator and this compiler, not about the program, and it is exactly the kind of fact that changes when you upgrade something. So the C transcripts are labelled with the compiler, target and optimization level that produced them, and nothing further is claimed. The Rust half of every page is verified normally, which is the asymmetry the section is about.

The compilers used, for anything you want to reproduce:

Apple clang version 21.0.0 (clang-2100.1.1.101), target x86_64-apple-darwin25.5.0
rustc 1.x stable, --edition 2024, no -O (so overflow checks are on)
clang version 23.1.0 on Compiler Explorer, for the two pages on C++'s replies, whose newest checks Apple's clang 21 lacks

How to read a page here

Each one runs the same five steps: the program (short enough to read in one go), what it did (a real run, with the command that produced it), why the standard allows it, what Rust does instead (a verified transcript), and the refusal — the actual rustc message for the same mistake. The two pages on C++'s replies swap what it did for what the check says, and end on where the C++ check and rustc part ways.

They are independent, so start with whichever bug you have personally shipped. If you have shipped none of them, use-after-free is the one with the most instructive output.

The honest limits

  • All of this means safe Rust. unsafe reopens every door — see what unsafe turns off. The guarantee is not that the doors are locked, it is that they are labelled.
  • C and C++ have tools for most of these, and they are good. Run against these nine programs on this machine: AddressSanitizer catches three outright — the double-free, the use-after-free, the buffer overrun — and annotates the null dereference's segfault after it has already happened. ThreadSanitizer catches the data race. UndefinedBehaviorSanitizer catches the signed overflow. Three are caught by none of them: the uninitialized read, because MemorySanitizer is the one that would and clang will not build it for x86_64-apple-darwin; the forgotten unlock, where ThreadSanitizer printed nothing in the four seconds before the hung process was killed; and the iterator invalidation, because nothing invalid is ever touched — only the answer is wrong. The difference from a build error is when and whether: a sanitizer finds a bug on the path a test happened to take, at a slowdown you cannot ship.
  • Modern C++ closes some of these by convention. unique_ptr makes double-free hard, lock_guard makes a forgotten unlock hard, and both are advice a codebase can decline to take on any given line. That is the actual contrast: the same idea, enforced rather than recommended. The two replies above are C++ starting to check instead of recommend — as warnings and build modes a codebase opts into, which is the part of the gap that remains.

See also

Po polsku

Ten dział nie uczy C ani C++ — pokazuje dziewięć konkretnych błędów, na które te języki pozwalają, uruchomionych naprawdę i z prawdziwym wydrukiem, a obok nich Rusta, który tych samych programów po prostu nie zbuduje. Strony są niezależne, więc zaczynaj od tego błędu, który sam kiedyś wypuściłeś na produkcję; jeśli żadnego, to use-after-free ma najbardziej wymowne wyjście — wypisał 6382657, czyli bajty łańcucha "Ada", które późniejszy malloc zdążył wstawić w zwolnione miejsce.

Rzecz, przez którą te strony jako jedyne w całej bibliotece nie mają zapisanego wzorca wyjścia: wszystkie dziewięć programów to zachowanie niezdefiniowane (undefined behaviour). Tu warto uważać na polskie nazewnictwo, bo norma C rozróżnia trzy rzeczy, które w polskich kursach zlewają się w jedną: zachowanie niezdefiniowane (norma nie mówi nic, wolno wszystko), zachowanie nieokreślone (unspecified — norma podaje zbiór dopuszczalnych wyników, kompilator wybiera jeden i nie musi mówić który) oraz zachowanie zdefiniowane przez implementację (implementation-defined — kompilator wybiera, ale ma obowiązek to udokumentować). Tylko pierwsze z nich znaczy „nie istnieje poprawny wynik”, i dlatego CI nie ma tu czego porównywać: 1433338037 z przepełnienia bufora jest faktem o tym kompilatorze i tej maszynie, nie o programie. Rustowa połowa każdej strony jest weryfikowana normalnie i ta asymetria jest właściwie tematem całego działu.

Dwa zastrzeżenia, żeby nie wyszło zbyt gładko. Po pierwsze, mowa o bezpiecznym Ruscie — unsafe otwiera te drzwi z powrotem; gwarancją nie jest to, że są zamknięte, tylko że są oznaczone. Po drugie, C i C++ mają na to narzędzia i są one dobre: na tych dziewięciu programach AddressSanitizer łapie trzy błędy, ThreadSanitizer wyścig danych, UndefinedBehaviorSanitizer przepełnienie ze znakiem — ale trzech nie łapie nikt, a różnica wobec błędu kompilacji dotyczy kiedy i czy: sanitizer znajduje błąd na tej ścieżce, którą akurat wykonał test, przy spowolnieniu, którego nie wypuścisz na produkcję. Tak samo unique_ptr i lock_guard w nowoczesnym C++ — ta sama idea, tyle że zalecana, a nie wymuszona.

Dwie ostatnie strony pokazują drugą stronę medalu — co robi samo C++. Safe Buffers to odpowiedź Clanga na przepełnienia bufora: długość zawsze razem z danymi, sprawdzany każdy indeks, surowy wskaźnik tylko za ostrzeżeniem. Lifetime safety to analiza czasów życia (lifetime analysis) wzorowana na rustowym borrow checkerze, działająca od Clanga 23. Obie są opcjonalne i obie kończą się tam, gdzie Rust wymusza więcej — i właśnie tę granicę te strony mierzą.

Szukaj po polsku: zachowanie niezdefiniowane w C · bezpieczeństwo pamięci · wyścig danych · undefined behavior C standard · rust memory safety · -fsanitize=address · C++ Safe Buffers · -Wlifetime-safety