From 29e588e4113ab56288c47b41661e1dbea54e9e93 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sat, 15 Aug 2026 10:41:27 +0530 Subject: [PATCH 01/14] Add a reality-check article on what Rust's memory-safety claim actually covers. Wire it into Compiler Comparisons with hub and sibling-article links so the slogan, the C++ comparison, and the rustc pipeline sit next to each other. Co-authored-by: Cursor --- docs/articles.md | 9 + docs/articles/rust-claims-a-reality-check.md | 598 ++++++++++++++++++ ...odern-cpp-memory-safety-beyond-the-hype.md | 4 + ...tc-pipeline-vs-cpp-compilation-pipeline.md | 2 +- sidebars/site.js | 1 + 5 files changed, 613 insertions(+), 1 deletion(-) create mode 100644 docs/articles/rust-claims-a-reality-check.md diff --git a/docs/articles.md b/docs/articles.md index a5bcf352..79144cc3 100644 --- a/docs/articles.md +++ b/docs/articles.md @@ -176,6 +176,15 @@ No article has been published from the Jenkins queue yet. Coming Soon Coming Soon + + + +Rust Claims +A Reality Check: Safety, Tools, and Systems Programming +Article +Coming Soon +Coming Soon +Coming Soon diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md new file mode 100644 index 00000000..abf15656 --- /dev/null +++ b/docs/articles/rust-claims-a-reality-check.md @@ -0,0 +1,598 @@ +--- +title: "Rust Claims, a Reality Check: Safety, Tools, and Systems Programming" +description: "A compiler-engineer reality check on Rust's memory-safety claim: threat model, the ISSTA 2026 rustc soundness study, rustc→MIR→LLVM failure boundaries, unsafe surface, FFI/deps, #25860, and the 2015 tools leftover." +keywords: + - rust memory safety claim + - rust unsafe surface + - rustc soundness hole 25860 + - ISSTA 2026 rustc unsound + - rust implied bounds trait objects + - rust MIR LLVM noalias + - stacked borrows tree borrows + - miri chalk a-mir-formality + - rust FFI dependency trust + - rust lending iterator + - rust compile time + - rust linux kernel + - rust vs c++ migration + - rust threat model + - rust reality check +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import Head from '@docusaurus/Head'; + + + + + +# Rust Claims, a Reality Check: Safety, Tools, and Systems Programming + +:::note +Related: [Rust vs Modern C++: Memory Safety Beyond the Hype](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [Rustc Pipeline vs C++ Compilation Pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). Those compare a cache and two compilers. This one tests the slogan against the type system, rustc, LLVM, and production `unsafe`. +::: + +I started this the way a lot of compiler people do: take the slogan at face value, then try to break it. + +**Rust is memory safe.** That is true enough to be useful, and incomplete enough to be abused. Safe Rust will refuse programs that C and C++ will happily compile. I wanted to know where that refusal actually stops — in `unsafe`, in a dependency, in FFI, or in rustc itself. + +The interesting question, after a few evenings of compiling things, was not “is the slogan false.” It was how many extra clauses you have to attach before it becomes a theorem. + +## Table of Contents + +- [0. What I actually came away with](#0-what-i-actually-came-away-with) +- [1. The claim](#1-the-claim) +- [2. What memory safety actually means](#2-what-memory-safety-actually-means) +- [3. Experiment: C vs C++ vs safe Rust](#3-experiment-c-vs-c-vs-safe-rust) +- [4. Failures that get sold as memory unsafety](#4-failures-that-get-sold-as-memory-unsafety) +- [5. The `unsafe` boundary](#5-the-unsafe-boundary) +- [6. Dependencies and FFI](#6-dependencies-and-ffi) +- [7. Case study: uutils](#7-case-study-uutils) +- [8. The compiler boundary](#8-the-compiler-boundary) +- [9. Research: ISSTA 2026 rustc soundness study](#9-research-issta-2026-rustc-soundness-study) +- [10. Case study: rustc #25860](#10-case-study-rustc-25860) +- [11. What moved, and what did not](#11-what-moved-and-what-did-not) +- [12. Systems language, tools, and 2026 leftovers](#12-systems-language-tools-and-2026-leftovers) +- [13. Where Rust wins, where C++ stays](#13-where-rust-wins-where-c-stays) +- [14. 2026 scorecard](#14-2026-scorecard) +- [15. How to evaluate the next claim](#15-how-to-evaluate-the-next-claim) +- [16. Limits](#16-limits) +- [17. References](#17-references) + +## 0. What I actually came away with + +If you only want the residue: safe Rust really does make UAF, spatial overflow, and data races on Rust-shared memory hard to write by accident. I could not get rustc 1.93.1 to accept a dangling local or `a[10]` on a `[T; 4]`. gcc and g++ 13.3 built both. + +What I expected to vanish, and did not: TOCTOU, silent `Result::ok()`, FFI length mistakes, and — once I stopped looking at application crates — rustc itself. [ISSTA 2026](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) is the census of that last layer (30 accept-invalid typeck reports; implied bounds and trait objects among the ones that can become memory unsafety). Miri helps *after* rustc already said yes. + +Android, Firefox, Cloudflare, and Rust-for-Linux did not adopt a bumper sticker. They adopted a default that moves one class of obligations onto the compiler. The compiler is a stack. That is the whole article. + +## 1. The claim + +The slide says: + +> Rust is memory safe. + +After writing the extra clauses out, I landed here — not as a slogan, as the only sentence I could defend: + +> **Safe Rust**, compiled by a **sound rustc**, with **no unsound `unsafe` in the crate or its dependencies**, does not exhibit use-after-free, spatial buffer overflow, data races on shared memory, null dereference, or reads of uninitialized memory. + +Every extra clause is a place I later found a hole. The borrow checker can be doing its job and that sentence can still be false. + +I also had to stop treating three internet fights as one. A 2015 tools rant (can you find `String::replace` in rustdoc search? is rustc still slow? can `Iterator` yield a borrow from `&mut self`?) is not a memory-safety argument. A 2023 “Rust is not a systems language” thread is not a borrow-checker argument. Mixing them is how you get heat and no measurement. + +## 2. What memory safety actually means + +Memory safety here means: loads, stores, and pointer operations stay inside the object the language can name, for the lifetime the language can prove, without a data race on that memory. It is not “the program does what you meant.” + +### 2.1 Threat model — which bug class? + +| Bug class | Safe Rust | `unsafe` Rust | C / C++ | +|---|---|---|---| +| Use-after-free | Generally prevented | Possible | Possible | +| Spatial buffer overflow | Generally prevented | Possible | Possible | +| Double free | Generally prevented | Possible | Possible | +| Data race (shared memory) | Generally prevented | Possible | Possible | +| Null dereference | Generally prevented | Possible | Possible | +| Uninitialized read | Generally prevented | Possible | Possible | +| Integer overflow | **Not generally prevented** (debug panic / release wrap; not C-style UB) | Same | Often UB | +| TOCTOU | Not prevented | Not prevented | Not prevented | +| Logic bug | Not prevented | Not prevented | Not prevented | +| Resource exhaustion | Not prevented | Not prevented | Not prevented | +| FFI contract violation | Not prevented at the boundary | Possible | Possible | +| Compiler soundness / miscompile | Not prevented | Not prevented | Not prevented | + +A panic on `slice[i]` is the *safe* failure. Continuing past a smashed heap canary is the unsafe one. OOM abort, leaks, and deadlocks were never in the theorem. + +### 2.2 The safety stack + +Rust’s memory-safety story is not a single mechanism. It is a stack of assumptions: + +```mermaid +flowchart TB + P[Rust program] --> S[Safe Rust] + S --> BC[Borrow checker + type checker + lifetimes] + BC --> RC[rustc soundness] + RC --> DEP["Dependencies + unsafe abstractions"] + DEP --> FFI[FFI / kernel / allocator] + FFI --> HW[OS + hardware] +``` + +I used to stop the picture at the borrow checker, because that is where the textbook examples stop. The first production crate I grepped (`unsafe`, `from_raw_parts`, `mmap`) made that feel silly. The hole can sit in rustc, in a dependency, or in a libc length, and the layer above is still “correct.” + +## 3. Experiment: C vs C++ vs safe Rust + +I compiled the next four on one machine: **rustc 1.93.1**, **gcc/g++ 13.3.0**, `-Wall -Wextra`, no sanitizers unless named. Not a SPEC run. I cared about a simpler question: does the frontend even argue. + +### 3.1 Use-after-free + + + + +```c +char *p = malloc(32); +free(p); +printf("%s", p); /* gcc: warning -Wuse-after-free; still links */ +``` + + + + +```cpp +auto* s = new std::string("secret"); +std::string_view v = *s; +delete s; +std::cout << v; // g++ 13.3: no warning, binary produced +``` + + + + +```rust +fn dangling() -> &'static str { + let s = String::from("secret"); + &s +} +``` + +```text +error[E0515]: cannot return reference to local variable `s` +``` + + + + +The part that surprised me was not rustc. It was g++: no diagnostic, binary on disk. gcc at least printed `-Wuse-after-free` and then linked anyway. ASan would have caught both C and C++ *if* I had turned it on and hit the path. I did not, on purpose — the slogan is about the default build, not the build you remember to sanitize. + +### 3.2 Constant bounds violation + + + + +```c +int a[4] = {0}; +a[10] = 42; /* gcc/g++ 13.3 -Wall -Wextra: no diagnostic, binary produced */ +``` + + + + +```rust +let mut a = [0; 4]; +a[10] = 42; +``` + +```text +error: this operation will panic at runtime + a[10] = 42; + ^^^^^ index out of bounds: the length is 4 but the index is 10 +note: `#[deny(unconditional_panic)]` on by default +``` + + + + +A runtime `a[i]` in safe Rust still compiles; it panics if `i` is hot. That is memory-safe. People sometimes paste the panic and call it a crash. I would rather have the panic than the smash gcc just emitted with no warning. + +### 3.3 TOCTOU — still open + +```text +stat(path) / access(path) ← check + │ + ▼ + attacker swaps the path + │ + ▼ +open(path) / chmod(path) ← use +``` + +Rust `std::fs` is path-shaped. The borrow checker does not see the inode. The 2026 uutils/Canonical CVE set is mostly this class. GNU coreutils has the same class *and* still ships spatial bugs; Rust removed one pile, not both. + +### 3.4 FFI — the guarantee stops + +```mermaid +flowchart LR + R[Safe Rust caller] --> API[Safe wrapper] + API --> U["unsafe + extern C"] + U --> C[C library] + C --> P[Raw pointer + length] +``` + +Past `extern "C"`, rustc is trusting a C ABI and a comment. Wrong `len`, a `NULL` that the man page calls success, a truncated `mmap` — none of that is a borrow-checker miss. If a talk only shows the first two tests, they showed the claim. The last two are where I spent the rest of the week. + +## 4. Failures that get sold as memory unsafety + +A few things I kept seeing in comment threads, sold as “so much for memory safety”: + +OOM abort and `slice[i]` panic are the *safe* outcomes. `mmap` SIGBUS after another process truncates the file is an `unsafe` + kernel invariant rustc cannot see. `File::from_raw_fd(stdin)` without `dup` is fd ownership, not aliasing UAF. `dd` computing LCM(`ibs`,`obs`) until the allocator gives up is DoS. Integer wrap that never becomes a slice length is defined wrap in release, not a smash. Leaks, deadlocks, logic, TOCTOU — never in the theorem I wrote down in §1. + +I am not trying to excuse those bugs. I am trying not to count them as the same bug as `strcpy` past a heap buffer. + +## 5. The `unsafe` boundary + +`unsafe` is not a confession that the language failed. It is the boundary where the compiler stops proving and starts trusting. + +The failure mode that actually hurts is a **safe function** that hides an unsound `unsafe` block. Callers write ordinary Rust and still get undefined behavior. The type system launders the lie: + +```rust +pub fn as_static(s: &str) -> &'static str { + unsafe { std::mem::transmute(s) } +} + +fn main() { + let dangling = { + let owned = String::from("secret-token-do-not-leak"); + as_static(&owned) + }; + println!("{dangling}"); +} +``` + +No `unsafe` in `main`. Still UAF. This is the first “gotcha” people mailed me. It does not test the slogan. It tests whether rustc re-proves the body of every `unsafe` block at every call site. It does not. It trusts the signature. `std` is full of `unsafe` for the same reason: hide the dangerous bit. The interesting failure is when that hiding is a lie. + +### 5.1 How much `unsafe` is too much? + +“500 `unsafe` blocks, still safer than C?” is the wrong yes/no. I have seen both: a crate with two tiny `unsafe` blocks behind a boring API, and a crate that is basically libc with a Rust accent. The first is the design. The second is C with extra steps. The thing I count, informally: + +```text +Unsafe surface ≈ + unsafe blocks + + unsafe fn + + extern / FFI boundaries + + from_raw_parts / offset / transmute + + invariants the compiler cannot see (mmap, fds, kernel) +``` + +Not a security metric. A way to stop arguing in the abstract. I would rather ship two audited `unsafe` blocks behind a boring safe API than a crate that reimplements libc in every module and still calls itself “memory safe because it is Rust.” + +## 6. Dependencies and FFI + +The claim already said: *no unsound `unsafe` in the crate **or its dependencies***. That clause needs its own diagram, because modern Rust is not `my code → rustc`. + +```mermaid +flowchart TB + M[Your crate] --> D[Direct crates.io deps] + D --> T[Transitive graph] + T --> U[Someone else's unsafe] + U --> F[FFI / C / kernel] + F --> OS[OS] +``` + +`cargo audit` finds *known* advisories. It does not prove the graph is sound. I have stopped treating “our crate has no `unsafe`” as a complete sentence once `Cargo.lock` is in the picture. + +FFI is the same story with a C ABI instead of a crate name. `CStr::from_ptr`, `from_raw_parts(ptr, len)`, `File::from_raw_fd` — the length came from libc. rustc never saw it. + +## 7. Case study: uutils + +One production data point, not a meta-analysis. + +Ubuntu 25.10 ships uutils (Rust coreutils). Canonical commissioned Zellic ahead of 26.04. The public write-up, [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/), is explicit: the CVE pile is TOCTOU, filesystem races, permission-after-create, GNU-parity logic, discarded `Result`s — including [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) (`dd` truncation swallowed with `Result::ok()`). The audit did **not** report buffer overflows, use-after-free, or uninitialized reads. GNU coreutils, over a comparable recent window, still shipped heap overwrites and OOB reads (`split --line-bytes`, `od --strings`, `unexpand --tabs`, `numfmt`). + +```mermaid +flowchart TB + A[uutils 2026 audit] --> B[Classic spatial / temporal memory] + A --> C[Semantic / system bugs] + B --> B1[largely absent] + C --> C1[TOCTOU / FFI length / logic / silent Result] +``` + +On a 2026 tree I grepped, `src/` still had on the order of two hundred `unsafe` keyword hits (libc, Win32, `mmap`, `from_raw_parts`). One that stuck: BSD `getmntinfo` can return `0` with `NULL`; a wrapper that only rejected `len < 0` then called `slice::from_raw_parts(null, 0)`. That is UB after a wrong libc check, not a missed borrow. + +When I started looking at Rust memory-safety claims, I expected the interesting bugs to disappear. They did not. The obvious UAF and bounds bugs became much harder to write. The remaining pile moved toward FFI, filesystem races, swallowed `Result`s, and — once I left application code — the compiler. That is what the audit actually supports. It does not support “Rust has no CVEs,” and it does not support “the rewrite was pointless.” + +## 8. The compiler boundary + +Safe Rust’s theorem is only as strong as the compiler that implements it. The implementation is a pipeline, and each stage can fail independently. + +```mermaid +flowchart TB + T[Rust type system / spec] --> R[rustc typeck + borrowck] + R --> M[MIR transforms] + M --> L[LLVM IR + noalias] + L --> O[LLVM optimizations] + O --> B[Backend / codegen] + B --> H[Machine code] +``` + +| Layer | Possible failure | +|---|---| +| Type system / spec | Design hole (implied bounds, variance) | +| rustc typeck / borrowck | Soundness bug: accepts a program the spec forbids | +| MIR transformation | Invalid rewrite of a well-typed program | +| LLVM IR generation | Wrong `noalias`, wrong provenance, wrong ABI | +| LLVM optimization | Miscompile under aliasing rustc promised | +| Backend | Wrong machine code | +| FFI | Contract the IR cannot see | +| `unsafe` abstraction | Invalid invariant rustc was told to trust | + +I used to treat `noalias` as a backend curiosity. Then I watched what a dangling `&'static` *means* once it has been blessed by typeck: LLVM is allowed to treat that pointer as a real object and delete “impossible” loads. Memory safety is not the same as memory-model correctness. `&mut` is a uniqueness theorem. rustc lowers that into LLVM `noalias`. Stacked Borrows and Tree Borrows are the operational stories of what those references may do. If the source-level story and the LLVM-level story disagree, “safe” code can be miscompiled, or `unsafe` that was careful under one model is UB under another. Miri can catch some of this. rustc + LLVM is what ships. + +A pointer is not an integer. That is the missing sentence in most Rust-vs-C threads. An implied-bounds hole is not a type-theory puzzle. It is a license for the optimizer. + +I mapped the IRs in the [rustc vs C++ pipeline piece](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). Below is the empirical evidence that typeck has, in fact, said yes to programs it should have rejected. + +## 9. Research: ISSTA 2026 rustc soundness study + +Yusung Sim (KAIST), Sukyoung Ryu (KAIST), and Jaemin Hong (UNIST), *[Rust's Type Checker Implementation is Unsound: An Empirical Study on Soundness Bugs in rustc](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i)*, ISSTA 2026 (Oakland, 3–9 October 2026, co-located with SPLASH). Artifact: [Zenodo 10.5281/zenodo.20698055](https://doi.org/10.5281/zenodo.20698055) (analysis sheets for RQ1–RQ4; files restricted at time of writing). + +I went looking for a measurement, not another anecdote. This paper is that measurement. It is not a study of buggy Rust *programs*. It is a study of buggy *type checking*: rustc accepted a program the type rules should have rejected. + +### 9.1 What they mean by a soundness bug + +Rust is marketed as a *type-sound* language: well-typed safe programs do not exhibit the undefined behaviors the type system is designed to rule out, including memory bugs. A **soundness bug in rustc** is narrower and more serious than an ICE or a false compile error: + +```text +program P should be rejected at typeck + │ + ▼ +rustc accepts P ← soundness bug (this paper) + │ + ▼ +P may then exhibit UB, a broken invariant, +or a memory bug — with no `unsafe` in P +``` + +A crash in rustc is a reliability bug. Rejecting a valid program is a completeness / false-reject bug. **Accepting an invalid program** is the soundness bug. Only the last one can launder a use-after-free through a green `cargo build`. + +Liu et al., *An Empirical Study of Bugs in the rustc Compiler* (OOPSLA 2025, [doi:10.1145/3763800](https://doi.org/10.1145/3763800)), is the broader rustc-bug census (crashes, miscompiles, false rejects). Sim, Ryu, and Hong *specialize* that space to type-soundness accept-invalid, and they explicitly reconcile their set against Liu et al. + +### 9.2 How the dataset was built + +Window: issues reported **1 January 2022 – 1 September 2025**, chosen to stay near recent rustc releases. + +```mermaid +flowchart LR + A["A-* typeck-related
969 issues"] --> B["C-bug or I-unsound
minus irrelevant labels
320"] + B --> C["Manual + dedup
23 soundness"] + C --> D["+ 7 from Liu et al.
that pass the same bar"] + D --> E["Final set
30"] +``` + +| Stage | Count | What it is | +|---|---|---| +| Crawl | 969 | rustc GitHub issues with area labels (`A-*`) tied to type checking | +| Label filter | 320 | Keep `C-bug` or `I-unsound`; drop labels that mark the issue as not a soundness report | +| Manual | 23 | Human read; duplicates removed; “is this actually accept-invalid?” | +| + Liu et al. | +7 of 18 unique-to-Liu | Same manual bar; **final N = 30** | + +I almost cited N=23 and stopped. That is what the conference abstract leads with. The artifact is explicit that they then folded in 7 issues from Liu et al. that passed the same manual bar, and the analysis set is 30. Both numbers are real. They measure different cuts. I use 30 as the analyzed set and 23 as the crawl-only set, and I wish the abstract had said that in one sentence. + +The artifact’s sheets answer **RQ1–RQ4** along the axes the paper names for every issue: + +| Axis | Question the authors ask | +|---|---| +| Affected feature | Which type-system feature is broken? | +| Symptom | *How* is it mishandled (wrong bound, skipped WF, bad coercion, …)? | +| Consequence | What undesirable behavior becomes possible? | +| Triggering features | Which *other* features must combine to fire it? | +| Community consensus | Did rustc maintainers treat it as a real soundness bug? | +| Lifecycle | When introduced, when found, when (if) fixed — Figure 1 in the paper | + +### 9.3 Five findings, read as a compiler threat model + +The abstract states five results. Mapped onto the safety stack in §2 and the pipeline in §8: + +| # | Finding | What it means for the slogan | +|---|---|---| +| 1 | Some soundness bugs, typically fired by **implied bounds** or **trait objects**, compromise **memory safety**. | Not every accept-invalid is a memory bug — but the ones that are, break the bumper sticker with no `unsafe` in the crate. | +| 2 | Sound typeck is strained by **associated types** and by **lifetimes interacting with traits**. | The hard region is not `Vec` indexing. It is the trait solver + region checker + well-formedness. | +| 3 | **Most bugs persist from the introduction of the feature** and take a long time to be discovered. | These are not “a bad weekend in 2024.” They are latent in the feature from day one. #25860 (2015) is the extreme of this shape, even though it sits outside their 2022–2025 *report* window. | +| 4 | **Miri** can detect the subset that becomes a memory bug at run time. **a-mir-formality** and **Chalk** are still too immature to serve as oracles for the other categories. | You cannot test rustc soundness by “running the program” unless the hole is observable as UB. Many holes are “wrong type accepted” with no immediate crash. | +| 5 | The **Rust Reference**, **FLS**, and **RFCs** often do not state the semantics precisely enough to be an oracle. | There is no complete, executable spec against which to differential-test rustc. The implementation *is* the spec more often than compiler engineers would like. | + +Finding 1 is why I put the paper in a memory-safety article at all. Finding 5 is why #25860 can sit open since 2015: if implied bounds plus variance are not an executable judgment, you cannot fail rustc with a spec test. You fail it with a program that should not compile and a human argument. That is a slow way to run a compiler test suite. + +**Implied bounds** (see the [rustc-dev-guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html)): from `&'a &'b T` the checker may assume `'b: 'a` without the user writing it. If that assumption is dropped under variance, fn-pointer reification, HRTB supertrait elaboration, or projection in an impl header, you get exactly the lifetime-laundering pattern in §10. Related open issues in the same family include [#100051](https://github.com/rust-lang/rust/issues/100051) (projections in impl headers) and [#84591](https://github.com/rust-lang/rust/issues/84591) (HRTB supertrait upcast). The guide itself lists those three as known unsoundnesses from implicit implied bounds. + +**Trait objects** (`dyn Trait`): well-formedness of the object type, lifetime bounds in the vtable, and “does `dyn Trait

: Trait

` imply the trait’s where-clauses?” have a long `I-unsound` history (e.g. [#44454](https://github.com/rust-lang/rust/issues/44454)). A hole here is not a style issue. It is a lie about what the vtable may be called with. + +**Associated types + lifetimes-in-traits**: projections can normalize on one path (call site) and stay unnormalized on another (impl checking), so an implied bound is assumed in the impl body and never proven at the use. That is a solver/WF bug, not an `unsafe` block. + +### 9.4 Oracles: what can actually test rustc? + +The paper’s second job is to ask whether existing artifacts could *find* these bugs, not just describe them after a human files GitHub issue #NNNNN. + +| Oracle | What it is | What the study reports | +|---|---|---| +| [Miri](https://github.com/rust-lang/miri) | MIR interpreter; Stacked/Tree Borrows | Useful **after** rustc accepts the program, if the hole is observable as UB / a memory bug. Not a typeck oracle: it never sees rejected programs. | +| [Chalk](https://github.com/rust-lang/chalk) | Logic-based trait solver (not production rustc) | Potential for trait-related holes; **immature** as a rustc soundness oracle. | +| [a-mir-formality](https://github.com/rust-lang/a-mir-formality) | Formal model of a Rust subset | Same: potential, not yet a drop-in differential test vs rustc. | +| Rust Reference | Informal language doc | Too imprecise to decide many edge cases. | +| [FLS](https://spec.ferrocene.dev/) (Ferrocene Language Specification) | Qualification-oriented spec | Same limit: not a complete executable type-soundness oracle. | +| RFCs | Design intent | Record what was *meant*; not what rustc *does* on the weird program. | + +C and C++ also lack a complete executable type-soundness spec, so I am not scoring this as a unique humiliation. The difference is the *claim*. Rust’s slogan depends on typeck being sound. If the oracles cannot decide the edge, what you have is an engineering process — issue tracker, types team, next-gen solver — not a finished theorem. + +```mermaid +flowchart TB + SPEC["Reference / FLS / RFCs
incomplete as oracles"] --> R[rustc typeck] + FORMAL["Chalk / a-mir-formality
immature"] -.->|cannot yet decide| R + R -->|accept-invalid| P[Well-typed according to rustc] + P --> MIRI[Miri] + MIRI -->|memory-bug subset| UB[UB / UAF observed] + MIRI -->|other soundness bugs| MISS[No crash — still unsound] +``` + +### 9.5 What this paper does *not* say + +- It does not say everyday `String` / `Vec` code is unsafe. The triggers are implied bounds, trait objects, associated types, lifetime–trait interaction. +- It does not measure how often these programs appear on crates.io. N=30 is a census of *reports*, not a prevalence study. +- It does not replace uutils. uutils is about production `unsafe` + TOCTOU. This paper is about rustc accepting *safe* programs it should reject. +- The full per-issue coding sheets live in a restricted artifact. Counts below the five abstract findings (exact split of memory-safety vs other consequences, median days-to-discovery) are not reproduced here as invented numbers. + +I am not going to pretend everyday `HashMap` code is in this set. I am also not going to pretend the set is empty. §10 is the one I actually compiled. + +## 10. Case study: rustc #25860 + +Ordinary application bugs live in *your* crate. Compiler soundness bugs live in *every* crate that hits the feature. + +| Category | Typical shape | +|---|---| +| Lifetimes / implied bounds | Nested references, outlives dropped under variance | +| Variance | Lifetime expansion through subtyping | +| HRTB | `for<'a>` fn-pointer / trait-bound coercion | +| Trait system | Associated types, projections that skip WF | +| Trait objects | Vtable / type-id assumptions | +| MIR | Invalid transform of a well-typed body | +| LLVM / codegen | Miscompile, wrong `noalias` | + +[#25860](https://github.com/rust-lang/rust/issues/25860) is the long-lived lifetime/variance case. Filed May 2015. Still open. The types team has treated a real fix as blocked on binders-with-where-clauses and the next-generation trait solver. [PR #156077](https://github.com/rust-lang/rust/pull/156077) (May 2026) was closed without landing; it did not bootstrap rustc. A June 2026 higher-ranked variant was closed as a duplicate. + +The `cve-rs` exploit uses **zero** `unsafe`. A sound helper + +```rust +fn lifetime_translator<'a, 'b, T: ?Sized>( + _val_a: &'a &'b (), + val_b: &'b T, +) -> &'a T { + val_b +} +``` + +is coerced to `for<'x> fn(_, &'x T) -> &'b T`. The implied `'b: 'a` is dropped. A `&&()` with `'static` then “proves” any lifetime: + +```rust +const STATIC_UNIT: &&() = &&(); + +pub fn as_static(x: &T) -> &'static T { + let f: for<'x> fn(_, &'x T) -> &'static T = lifetime_translator; + f(STATIC_UNIT, x) +} +``` + +I compiled this on **rustc 1.93.1**. It accepted it. Zero `unsafe`. After dropping the `String` and allocating something the same size, debug aborted inside `ptr::copy_nonoverlapping`; release printed zeroes. That was the moment the “if it compiled, rustc proved it” sentence died for me — not as a claim about `Vec`, as a claim about rustc. + +Everyday application code does not look like HRTB fn-pointer coercion. If you lead a *tools* argument with this file, a competent reply is “compiler bug.” Fair. Lead with rustdoc search and compile wait if that is the argument. I am keeping the file here because I ran it, and because ISSTA 2026 is why I no longer treat it as a one-off. + +## 11. What moved, and what did not + +I already said this in the uutils section, so I will not dress it up again. Spatial and temporal memory bugs got much rarer in the safe subset. TOCTOU, GNU-parity logic, resource exhaustion, and FFI contracts did not. §3 is the lab version of the first half. uutils is the production version of the second. Waiting on rustc and not finding `replace` from a `String` search box is a third axis entirely — developer time, not CVEs. + +## 12. Systems language, tools, and 2026 leftovers + +### 12.1 The 2023 thread, compressed + +[Why are some people against the Rust-Lang?](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) started from awkward `asm!`, missing protected-mode ops, and “Linux has apt-get, they didn’t add it.” ZiCog’s reply aged well: protected-mode code is a tiny kernel fraction; Linus saying Rust is “not altogether a bad idea” is, for Linus, praise; **Rust and C live together**; nobody is annotating a billion lines of old C. + +2026: Rust-for-Linux is in-tree, politically noisy, still mostly C. `asm!` is not the 2015 toy page; privileged ops still live in `.S` / `global_asm!` / a C stub, same as C kernels. kornel’s non-technical pile (anti-hype, C careers, “bugs are bad programmers”) still describes the internet. It does not decide whether `Vec` indexing is bounds-checked. + +### 12.2 2015 tools, scored without theology + +The useful mid-2010s post skipped the borrow checker and judged the tools. 2026: + +| Then | Now | Verdict | +|---|---|---| +| rustdoc: known unknowns; `replace` lives on `str` | `String` lists Deref methods; **search still ignores Deref** | Half-fixed. rust-analyzer is the real fix. | +| rustc is slow; future looks promising | Parallel frontend ~20–30% in tests, not default; Cranelift a few % | Still a tool defect. Promising for a decade. | +| Need type-based serialize | serde + derive | Solved. Fight is zero-copy. | +| Cannot ship apps | `cargo install`, [cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall) | Solved enough if you opt in. | +| Abstract return types | `impl Trait`, stable 1.26 | Solved. | +| Streaming / lending `Iterator` | GATs exist; **std has no `LendingIterator`** | Same hole. [rust-streaming](https://github.com/emk/rust-streaming). | +| `num` / const generics | `const N: usize` yes; `generic_const_exprs` incomplete | Mostly moved on. | + +I actually typed `replace` into rustdoc search on a `String` page. Deref methods are listed if you already know to scroll. Search still does not walk Deref. rust-analyzer does. That was the original hypothesis — rustdoc is good for known unknowns — and it is still true on the website. + +If you want to argue in *that* 2015 voice, those three leftovers are the argument. #25860 is a different argument. I keep mixing them in conversation; I am trying not to on the page. + +## 13. Where Rust wins, where C++ stays + +Do not ask “is Rust better than C++?” Ask **which component benefits from stronger invariants.** + +```mermaid +flowchart TB + SYS[Existing C++ system] + SYS --> UI[UI / application] + SYS --> BL[Business logic] + SYS --> NET[Networking] + SYS --> PAR[Parser] + SYS --> CORE[Memory-critical core] + SYS --> HW[Hardware / FFI / SDK] + CORE -.->|often| R[Rust candidate] + PAR -.->|often| R + NET -.->|sometimes| R + HW -.->|usually stay C/C++ or wrap| C[Keep C ABI] +``` + +I would reach for Rust on a new parser, a concurrent cache, anything where ownership is the actual problem and the C ABI surface is small enough to wrap. I would not reach for it as a moral upgrade of a 400 kLoC platform SDK wrapper, or a SIMD kernel that is already correct in C++ and paid for. Compile time is not a footnote on those teams. Hardware poke is not a footnote either. + +### 13.1 Selective entry, not rewrite + +“Rewrite it in Rust” is a meme I am tired of arguing with. New drivers, new Android native code, a sealed cache — those are plans. The [cache comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) is the piece I would send for one greenfield component. + +### 13.2 Could C++ adopt the ideas? + +It already adopted some: RAII, `unique_ptr` / `shared_ptr`, `std::span`, `string_view` (which also makes dangling easier to type), sanitizers, lifetime profiles, contracts experiments, Safe C++ / circle-style borrow checking. The remaining question is defaults: does C++ need Rust, or does C++ need the dangerous path to stop being the default? Both can be true. Sanitizers are opt-in and miss untested paths; rustc is opt-out for safe code and still has holes in rustc itself. + +## 14. 2026 scorecard + +| Claim you hear | Fair reading | +|---|---| +| Rust is memory safe. | **Safe** Rust is, modulo rustc soundness and unsound `unsafe` in the dep graph. | +| If it compiles, it cannot dangle. | False as an absolute. True for ordinary safe code that does not hit a compiler hole. ISSTA 2026: 30 rustc accept-invalid reports; #25860 is still open. | +| `unsafe` in `std` means the language is a con. | No. Encapsulation is the point. Unsound encapsulation is the bug. Count surface area. | +| Rust has no memory bugs in production. | Fewer *spatial/temporal* ones. Plenty of TOCTOU, logic, FFI-length. | +| Rust is not a systems language. | It is. It is not a drop-in for every privileged instruction or every C tree. | +| rustdoc and cargo solved onboarding. | rust-analyzer did more than rustdoc search. Compile time and lending iterators did not. | +| Rewrite it in Rust. | Usually a meme. New, sealed components are the realistic path. | + +## 15. How to evaluate the next claim + +When the next post says Rust “solved memory safety,” I now ask a smaller set of questions than I used to. Is this safe code or a kernel wrapper? Which rustc, and is the hole still open — ISSTA 2026 is the census for 2022–2025 reports; #25860 still is. Which bug class — a TOCTOU CVE does not refute a bounds check, and a GNU `split` heap overwrite does not vanish because Rust can panic. How much `unsafe` and FFI is actually in the tree. What did typeck, MIR, and `noalias` have to get right. + +When the post says Rust is overhyped, I ask the reverse: did they show a safe, no-`unsafe`, not-a-compiler-bug UAF? The `transmute` snippet in §5 is not that demo. I compiled that one too. It is the escape hatch. + +## 16. Limits + +- UAF/OOB snippets: rustc 1.93.1 and gcc/g++ 13.3.0 on one machine; no sanitizers in the “compiles” column. +- #25860 checked on rustc 1.93.1; the tracking issue remains open. +- ISSTA 2026 details follow the [conference abstract](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) and [artifact record](https://doi.org/10.5281/zenodo.20698055). The per-issue coding sheets were restricted; this article does not invent splits or medians that the public abstract does not state. Abstract N=23 (crawl) vs artifact N=30 (crawl + Liu complement) are both reported. +- uutils remarks are from 2026 public audit writing, not a claim that every Rust CLI is clean. +- Compile-time and rustdoc-search behavior change release to release. +- This article does not measure serde vs rkyv, count in-tree Linux Rust drivers, or treat a soundness-hole SIGSEGV as evidence that typical application Rust is unsafe. + +I keep coming back to the 2023 thread because it already had the stance I ended up with, before I had compiled anything: C and Rust can live together; programmer time is still the expensive input; compile-time checking is a bet that machines got cheaper faster than attention did. I just wanted the theorem written down, with the extra clauses visible. + +## 17. References + +1. [Why are some people against the Rust-Lang?](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906), users.rust-lang.org, May 2023. +2. [rust-lang/rust#25860](https://github.com/rust-lang/rust/issues/25860), implied bounds + variance (open since 2015). +3. [PR #156077](https://github.com/rust-lang/rust/pull/156077), attempted fix (closed May 2026, did not land). +4. [cve-rs](https://github.com/Speykious/cve-rs), lifetime expansion in safe Rust. +5. Yusung Sim, Sukyoung Ryu, Jaemin Hong, [Rust's Type Checker Implementation is Unsound: An Empirical Study on Soundness Bugs in rustc](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i), ISSTA 2026 (KAIST / UNIST). +6. Artifact, [10.5281/zenodo.20698055](https://doi.org/10.5281/zenodo.20698055) (RQ1–RQ4 sheets; 969 → 320 → 23 → 30). +7. Zixi Liu, Yang Feng, Yunbo Ni, Shaohua Li, Xizhe Yin, Qingkai Shi, Baowen Xu, Zhendong Su, [An Empirical Study of Bugs in the rustc Compiler](https://doi.org/10.1145/3763800), Proc. ACM Program. Lang. 9, OOPSLA2 (2025). +8. rustc-dev-guide, [Implied bounds](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) (lists #25860, #84591, #100051). +9. [Miri](https://github.com/rust-lang/miri), [Chalk](https://github.com/rust-lang/chalk), [a-mir-formality](https://github.com/rust-lang/a-mir-formality); [Ferrocene Language Specification](https://spec.ferrocene.dev/). +10. [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/), uutils / Canonical CVE set. +11. [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc), uutils `dd` / `Result::ok()`. +12. rustdoc book, [Search](https://doc.rust-lang.org/nightly/rustdoc/read-documentation/search.html) (Deref ignored). +13. [rust-lang/rust#19190](https://github.com/rust-lang/rust/issues/19190), rustdoc methods via auto-deref. +14. Rust project goals, [Promoting Parallel Front End (2026)](https://rust-lang.github.io/rust-project-goals/2026/parallel-front-end.html). +15. Nicholas Nethercote, [How to speed up the Rust compiler in July 2026](https://nnethercote.github.io/2026/07/31/how-to-speed-up-the-rust-compiler-in-july-2026.html). +16. [axodotdev/cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall). +17. [emk/rust-streaming](https://github.com/emk/rust-streaming). +18. Linus Walleij, [*Rust in Perspective*](https://people.kernel.org/linusw/rust-in-perspective). +19. Ralf Jung et al., Stacked Borrows; Tree Borrows (operational aliasing models; Miri). +20. [Rust vs Modern C++: Memory Safety Beyond the Hype](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype). +21. [Rustc Pipeline vs C++ Compilation Pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). diff --git a/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype.md b/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype.md index 53b95b5a..ae7d2ddf 100644 --- a/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype.md +++ b/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype.md @@ -60,6 +60,10 @@ Roadmap: Traditional C++ -> memory bugs -> sanitizers and safer APIs -> modern C Bug index: [use-after-free](#4-use-after-free), [buffer overflow](#5-buffer-overflow), [null pointer dereference](#6-null-pointer-dereference), [data race](#7-data-race), [memory leak](#memory-leak-vs-use-after-free) ::: +:::note +Related: [Rust Claims, a Reality Check](/docs/articles/rust-claims-a-reality-check) covers the slogan after the comparison: what safe Rust actually proves, which 2015 tool complaints remain, and the 2023 systems-language thread. +::: + ## 0. Abstract This article compares Rust and modern C++ on one concrete system: a concurrent in-memory cache with a background eviction thread. It measures the kinds of failures that matter in native code, not raw speed: [use-after-free](#4-use-after-free), [buffer overflow](#5-buffer-overflow), [null pointer dereference](#6-null-pointer-dereference), and [data races](#7-data-race). For each case, it shows the bug in traditional C++, the kind of signal a sanitizer can give you, the modern C++ fix, and the Rust shape that often prevents the bug from compiling in safe code. The article does not claim that Rust eliminates all bugs, that modern C++ cannot be made safer, or that one language always wins. It is meant to help the reader decide when compile-time enforcement is worth the migration cost, when toolchain hardening is enough, and when the safer default should be a language choice rather than a build flag. diff --git a/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline.md b/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline.md index 011bb8f1..5ade30ab 100644 --- a/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline.md +++ b/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline.md @@ -59,7 +59,7 @@ The two pipelines look similar at a distance because both frontends eventually l For C++, the compiler mostly checks syntax and type correctness, then enforces a mix of language rules and library-level ownership patterns before handing off to the backend. For Rust, those guarantees are part of the compilation model itself: ownership, borrowing, and lifetimes are validated before code generation, and Rust often passes through multiple IRs before it ever reaches LLVM. :::note -Both pipelines eventually reach native machine code, but the important difference is where each compiler enforces program invariants. +Both pipelines eventually reach native machine code, but the important difference is where each compiler enforces program invariants. For the slogan those checks get sold as, see [Rust Claims, a Reality Check](/docs/articles/rust-claims-a-reality-check). ::: ## Key Takeaway diff --git a/sidebars/site.js b/sidebars/site.js index 54c97e98..e7335335 100644 --- a/sidebars/site.js +++ b/sidebars/site.js @@ -217,6 +217,7 @@ const site = { collapsed: false, items: [ 'articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype', + 'articles/rust-claims-a-reality-check', 'articles/rustc-pipeline-vs-cpp-compilation-pipeline', ], }, From b4e75bcdc98bd7c6166e282d2e999e1767f5d0aa Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sat, 15 Aug 2026 10:44:07 +0530 Subject: [PATCH 02/14] Rewrite the Rust claims article in a first-person investigation voice. Drop the report cadence (paired tables, wrap-up boxes, extra diagrams) while keeping the compiled demos, uutils notes, and ISSTA / #25860 facts. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 553 ++++--------------- 1 file changed, 117 insertions(+), 436 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index abf15656..146006db 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -1,22 +1,13 @@ --- title: "Rust Claims, a Reality Check: Safety, Tools, and Systems Programming" -description: "A compiler-engineer reality check on Rust's memory-safety claim: threat model, the ISSTA 2026 rustc soundness study, rustc→MIR→LLVM failure boundaries, unsafe surface, FFI/deps, #25860, and the 2015 tools leftover." +description: "What 'Rust is memory safe' actually covers after compiling the demos, reading the uutils audit, and sitting with the ISSTA 2026 rustc soundness study." keywords: - - rust memory safety claim - - rust unsafe surface - - rustc soundness hole 25860 - - ISSTA 2026 rustc unsound - - rust implied bounds trait objects - - rust MIR LLVM noalias - - stacked borrows tree borrows - - miri chalk a-mir-formality - - rust FFI dependency trust + - rust memory safety + - rustc soundness 25860 + - ISSTA 2026 rustc + - rust unsafe FFI - rust lending iterator - rust compile time - - rust linux kernel - - rust vs c++ migration - - rust threat model - - rust reality check --- import Tabs from '@theme/Tabs'; @@ -24,130 +15,103 @@ import TabItem from '@theme/TabItem'; import Head from '@docusaurus/Head'; - + # Rust Claims, a Reality Check: Safety, Tools, and Systems Programming :::note -Related: [Rust vs Modern C++: Memory Safety Beyond the Hype](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [Rustc Pipeline vs C++ Compilation Pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). Those compare a cache and two compilers. This one tests the slogan against the type system, rustc, LLVM, and production `unsafe`. +Related: [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [Rustc vs C++ pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) ::: -I started this the way a lot of compiler people do: take the slogan at face value, then try to break it. +I started this the way I start most compiler arguments: take the slogan literally, then try to make rustc accept something it should not. -**Rust is memory safe.** That is true enough to be useful, and incomplete enough to be abused. Safe Rust will refuse programs that C and C++ will happily compile. I wanted to know where that refusal actually stops — in `unsafe`, in a dependency, in FFI, or in rustc itself. +**Rust is memory safe.** After a few evenings with rustc 1.93.1 and gcc 13.3, that sentence is still useful. It is also missing half its assumptions. I wanted the missing half on the page — not as a takedown, as the extra clauses I had to write down before I could defend the claim. -The interesting question, after a few evenings of compiling things, was not “is the slogan false.” It was how many extra clauses you have to attach before it becomes a theorem. +I also had to stop treating three fights as one. A 2015 tools rant (can you find `replace` from a `String` page? is rustc still slow?) is not a memory-safety argument. A 2023 “not a systems language” thread is not a borrow-checker argument. Mixing them is how comment sections stay loud. ## Table of Contents -- [0. What I actually came away with](#0-what-i-actually-came-away-with) -- [1. The claim](#1-the-claim) -- [2. What memory safety actually means](#2-what-memory-safety-actually-means) -- [3. Experiment: C vs C++ vs safe Rust](#3-experiment-c-vs-c-vs-safe-rust) -- [4. Failures that get sold as memory unsafety](#4-failures-that-get-sold-as-memory-unsafety) -- [5. The `unsafe` boundary](#5-the-unsafe-boundary) -- [6. Dependencies and FFI](#6-dependencies-and-ffi) -- [7. Case study: uutils](#7-case-study-uutils) -- [8. The compiler boundary](#8-the-compiler-boundary) -- [9. Research: ISSTA 2026 rustc soundness study](#9-research-issta-2026-rustc-soundness-study) -- [10. Case study: rustc #25860](#10-case-study-rustc-25860) -- [11. What moved, and what did not](#11-what-moved-and-what-did-not) -- [12. Systems language, tools, and 2026 leftovers](#12-systems-language-tools-and-2026-leftovers) -- [13. Where Rust wins, where C++ stays](#13-where-rust-wins-where-c-stays) -- [14. 2026 scorecard](#14-2026-scorecard) -- [15. How to evaluate the next claim](#15-how-to-evaluate-the-next-claim) -- [16. Limits](#16-limits) -- [17. References](#17-references) +- [Where I landed](#where-i-landed) +- [The sentence I can actually defend](#the-sentence-i-can-actually-defend) +- [Which bugs are even in scope](#which-bugs-are-even-in-scope) +- [Four programs I compiled](#four-programs-i-compiled) +- [`unsafe`, deps, FFI](#unsafe-deps-ffi) +- [uutils](#uutils) +- [The compiler is in the threat model](#the-compiler-is-in-the-threat-model) +- [ISSTA 2026](#issta-2026) +- [#25860, on this machine](#25860-on-this-machine) +- [Tools leftover from 2015](#tools-leftover-from-2015) +- [Would I use it](#would-i-use-it) +- [Limits](#limits) +- [References](#references) -## 0. What I actually came away with +## Where I landed -If you only want the residue: safe Rust really does make UAF, spatial overflow, and data races on Rust-shared memory hard to write by accident. I could not get rustc 1.93.1 to accept a dangling local or `a[10]` on a `[T; 4]`. gcc and g++ 13.3 built both. +Safe Rust really does make use-after-free, spatial overflow, and data races on Rust-shared memory hard to write by accident. I could not get rustc to accept a dangling local or `a[10]` on a `[T; 4]`. gcc and g++ built both. -What I expected to vanish, and did not: TOCTOU, silent `Result::ok()`, FFI length mistakes, and — once I stopped looking at application crates — rustc itself. [ISSTA 2026](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) is the census of that last layer (30 accept-invalid typeck reports; implied bounds and trait objects among the ones that can become memory unsafety). Miri helps *after* rustc already said yes. +What I expected to vanish, and did not: TOCTOU, swallowed `Result`s, FFI length mistakes, and — once I stopped grepping application crates — rustc itself. [ISSTA 2026](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) is the census of that last layer. Miri helps *after* rustc already said yes. -Android, Firefox, Cloudflare, and Rust-for-Linux did not adopt a bumper sticker. They adopted a default that moves one class of obligations onto the compiler. The compiler is a stack. That is the whole article. +Android and Rust-for-Linux did not adopt a slogan. They adopted a default that moves one class of work onto the compiler. The compiler is a stack. That is the residue. -## 1. The claim +## The sentence I can actually defend -The slide says: - -> Rust is memory safe. - -After writing the extra clauses out, I landed here — not as a slogan, as the only sentence I could defend: +The slide says “Rust is memory safe.” After writing the extra clauses out: > **Safe Rust**, compiled by a **sound rustc**, with **no unsound `unsafe` in the crate or its dependencies**, does not exhibit use-after-free, spatial buffer overflow, data races on shared memory, null dereference, or reads of uninitialized memory. Every extra clause is a place I later found a hole. The borrow checker can be doing its job and that sentence can still be false. -I also had to stop treating three internet fights as one. A 2015 tools rant (can you find `String::replace` in rustdoc search? is rustc still slow? can `Iterator` yield a borrow from `&mut self`?) is not a memory-safety argument. A 2023 “Rust is not a systems language” thread is not a borrow-checker argument. Mixing them is how you get heat and no measurement. - -## 2. What memory safety actually means - -Memory safety here means: loads, stores, and pointer operations stay inside the object the language can name, for the lifetime the language can prove, without a data race on that memory. It is not “the program does what you meant.” +## Which bugs are even in scope -### 2.1 Threat model — which bug class? +Memory safety here means loads and stores stay inside the object the language can name, for the lifetime it can prove, without a data race on that memory. It is not “the program does what you meant.” -| Bug class | Safe Rust | `unsafe` Rust | C / C++ | +| Bug class | Safe Rust | `unsafe` / FFI | C / C++ | |---|---|---|---| -| Use-after-free | Generally prevented | Possible | Possible | -| Spatial buffer overflow | Generally prevented | Possible | Possible | -| Double free | Generally prevented | Possible | Possible | -| Data race (shared memory) | Generally prevented | Possible | Possible | -| Null dereference | Generally prevented | Possible | Possible | -| Uninitialized read | Generally prevented | Possible | Possible | -| Integer overflow | **Not generally prevented** (debug panic / release wrap; not C-style UB) | Same | Often UB | -| TOCTOU | Not prevented | Not prevented | Not prevented | -| Logic bug | Not prevented | Not prevented | Not prevented | -| Resource exhaustion | Not prevented | Not prevented | Not prevented | -| FFI contract violation | Not prevented at the boundary | Possible | Possible | -| Compiler soundness / miscompile | Not prevented | Not prevented | Not prevented | +| UAF, spatial overflow, double-free, data race, null, uninit | Generally prevented | Possible | Possible | +| Integer overflow | Debug panic / release wrap — not C-style UB | Same | Often UB | +| TOCTOU, logic, resource exhaustion | Not in the theorem | Same | Same | +| FFI contract / compiler soundness | Not prevented | Possible | Possible | -A panic on `slice[i]` is the *safe* failure. Continuing past a smashed heap canary is the unsafe one. OOM abort, leaks, and deadlocks were never in the theorem. +A panic on `slice[i]` is the safe outcome. Continuing past a smashed canary is the other one. OOM abort, leaks, and deadlocks were never in the sentence above. -### 2.2 The safety stack - -Rust’s memory-safety story is not a single mechanism. It is a stack of assumptions: +I used to stop the picture at the borrow checker. The first production crate I grepped (`unsafe`, `from_raw_parts`, `mmap`) made that feel silly. The hole can sit in rustc, in a dependency, or in a libc length, and the layer above is still “correct.” ```mermaid flowchart TB - P[Rust program] --> S[Safe Rust] - S --> BC[Borrow checker + type checker + lifetimes] + P[program] --> S[safe Rust] + S --> BC[borrowck / typeck] BC --> RC[rustc soundness] - RC --> DEP["Dependencies + unsafe abstractions"] - DEP --> FFI[FFI / kernel / allocator] - FFI --> HW[OS + hardware] + RC --> DEP[deps + unsafe] + DEP --> FFI[FFI / kernel] + FFI --> HW[OS] ``` -I used to stop the picture at the borrow checker, because that is where the textbook examples stop. The first production crate I grepped (`unsafe`, `from_raw_parts`, `mmap`) made that feel silly. The hole can sit in rustc, in a dependency, or in a libc length, and the layer above is still “correct.” - -## 3. Experiment: C vs C++ vs safe Rust - -I compiled the next four on one machine: **rustc 1.93.1**, **gcc/g++ 13.3.0**, `-Wall -Wextra`, no sanitizers unless named. Not a SPEC run. I cared about a simpler question: does the frontend even argue. +## Four programs I compiled -### 3.1 Use-after-free +Same machine: **rustc 1.93.1**, **gcc/g++ 13.3.0**, `-Wall -Wextra`, no sanitizers unless I say so. Not a SPEC run. I only cared whether the frontend argued. - + ```c char *p = malloc(32); free(p); -printf("%s", p); /* gcc: warning -Wuse-after-free; still links */ +printf("%s", p); /* gcc: -Wuse-after-free, then a binary */ ``` - + ```cpp auto* s = new std::string("secret"); std::string_view v = *s; delete s; -std::cout << v; // g++ 13.3: no warning, binary produced +std::cout << v; // g++ 13.3: no warning ``` - + ```rust fn dangling() -> &'static str { @@ -163,303 +127,83 @@ error[E0515]: cannot return reference to local variable `s` -The part that surprised me was not rustc. It was g++: no diagnostic, binary on disk. gcc at least printed `-Wuse-after-free` and then linked anyway. ASan would have caught both C and C++ *if* I had turned it on and hit the path. I did not, on purpose — the slogan is about the default build, not the build you remember to sanitize. +The surprise was not rustc. It was g++: no diagnostic, binary on disk. gcc at least complained and then linked anyway. ASan would have caught both *if* I had turned it on. I did not, on purpose. The slogan is about the default build. -### 3.2 Constant bounds violation - - - - -```c -int a[4] = {0}; -a[10] = 42; /* gcc/g++ 13.3 -Wall -Wextra: no diagnostic, binary produced */ -``` - - - - -```rust -let mut a = [0; 4]; -a[10] = 42; -``` +Constant `a[10]` on a four-element array: gcc/g++ still silent. rustc: ```text error: this operation will panic at runtime a[10] = 42; - ^^^^^ index out of bounds: the length is 4 but the index is 10 note: `#[deny(unconditional_panic)]` on by default ``` - - - -A runtime `a[i]` in safe Rust still compiles; it panics if `i` is hot. That is memory-safe. People sometimes paste the panic and call it a crash. I would rather have the panic than the smash gcc just emitted with no warning. - -### 3.3 TOCTOU — still open - -```text -stat(path) / access(path) ← check - │ - ▼ - attacker swaps the path - │ - ▼ -open(path) / chmod(path) ← use -``` - -Rust `std::fs` is path-shaped. The borrow checker does not see the inode. The 2026 uutils/Canonical CVE set is mostly this class. GNU coreutils has the same class *and* still ships spatial bugs; Rust removed one pile, not both. - -### 3.4 FFI — the guarantee stops - -```mermaid -flowchart LR - R[Safe Rust caller] --> API[Safe wrapper] - API --> U["unsafe + extern C"] - U --> C[C library] - C --> P[Raw pointer + length] -``` - -Past `extern "C"`, rustc is trusting a C ABI and a comment. Wrong `len`, a `NULL` that the man page calls success, a truncated `mmap` — none of that is a borrow-checker miss. If a talk only shows the first two tests, they showed the claim. The last two are where I spent the rest of the week. +A runtime `a[i]` in safe Rust still compiles and panics if `i` is hot. People paste that panic and call it a crash. I would rather have the panic than the smash gcc just emitted. -## 4. Failures that get sold as memory unsafety +TOCTOU is still open. `std::fs` is path-shaped. The borrow checker does not see the inode. The 2026 uutils/Canonical CVE set is mostly that class. GNU coreutils has the same class *and* still ships spatial bugs. -A few things I kept seeing in comment threads, sold as “so much for memory safety”: +Past `extern "C"`, rustc is trusting a C ABI and a comment. Wrong `len`, a `NULL` the man page calls success, a truncated `mmap` — none of that is a missed borrow. If a talk only shows the first two tests, they showed the claim. The last two are where I spent the rest of the week. -OOM abort and `slice[i]` panic are the *safe* outcomes. `mmap` SIGBUS after another process truncates the file is an `unsafe` + kernel invariant rustc cannot see. `File::from_raw_fd(stdin)` without `dup` is fd ownership, not aliasing UAF. `dd` computing LCM(`ibs`,`obs`) until the allocator gives up is DoS. Integer wrap that never becomes a slice length is defined wrap in release, not a smash. Leaks, deadlocks, logic, TOCTOU — never in the theorem I wrote down in §1. +Comment threads keep selling OOM abort, `mmap` SIGBUS, `File::from_raw_fd(stdin)` without `dup`, and `dd` allocating until the process dies as “so much for memory safety.” Those are real defects. They are not `strcpy` past a heap buffer. I am not trying to excuse them. I am trying not to count them twice. -I am not trying to excuse those bugs. I am trying not to count them as the same bug as `strcpy` past a heap buffer. +## `unsafe`, deps, FFI -## 5. The `unsafe` boundary - -`unsafe` is not a confession that the language failed. It is the boundary where the compiler stops proving and starts trusting. - -The failure mode that actually hurts is a **safe function** that hides an unsound `unsafe` block. Callers write ordinary Rust and still get undefined behavior. The type system launders the lie: +The first “gotcha” people sent me was this: ```rust pub fn as_static(s: &str) -> &'static str { unsafe { std::mem::transmute(s) } } - -fn main() { - let dangling = { - let owned = String::from("secret-token-do-not-leak"); - as_static(&owned) - }; - println!("{dangling}"); -} ``` -No `unsafe` in `main`. Still UAF. This is the first “gotcha” people mailed me. It does not test the slogan. It tests whether rustc re-proves the body of every `unsafe` block at every call site. It does not. It trusts the signature. `std` is full of `unsafe` for the same reason: hide the dangerous bit. The interesting failure is when that hiding is a lie. +No `unsafe` in `main`. Still UAF. That does not test the slogan. It tests whether rustc re-proves the body of every `unsafe` block at every call site. It does not. It trusts the signature. `std` is full of `unsafe` for the same reason: hide the dangerous bit. The interesting failure is when that hiding is a lie. -### 5.1 How much `unsafe` is too much? +“500 `unsafe` blocks, still safer than C?” is the wrong yes/no. I have seen a crate with two tiny blocks behind a boring API, and a crate that is libc with a Rust accent. Informally I count blocks, `unsafe fn`, `extern`, `from_raw_parts` / `transmute`, and invariants rustc cannot see (`mmap`, fds). Not a security metric. A way to stop arguing in the abstract. -“500 `unsafe` blocks, still safer than C?” is the wrong yes/no. I have seen both: a crate with two tiny `unsafe` blocks behind a boring API, and a crate that is basically libc with a Rust accent. The first is the design. The second is C with extra steps. The thing I count, informally: +`cargo audit` finds known advisories. It does not prove `Cargo.lock` is sound. I have stopped treating “our crate has no `unsafe`” as a complete sentence. FFI is the same story with a C ABI instead of a crate name. The length came from libc. rustc never saw it. -```text -Unsafe surface ≈ - unsafe blocks - + unsafe fn - + extern / FFI boundaries - + from_raw_parts / offset / transmute - + invariants the compiler cannot see (mmap, fds, kernel) -``` +## uutils -Not a security metric. A way to stop arguing in the abstract. I would rather ship two audited `unsafe` blocks behind a boring safe API than a crate that reimplements libc in every module and still calls itself “memory safe because it is Rust.” +Ubuntu 25.10 ships the Rust coreutils. Canonical had Zellic look at them ahead of 26.04. [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) is explicit: the CVE pile is TOCTOU, filesystem races, GNU-parity logic, discarded `Result`s — including [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) (`dd` truncation swallowed with `.ok()`). They did **not** report buffer overflows, UAF, or uninitialized reads. GNU, over a comparable window, still shipped heap overwrites (`split --line-bytes`, `od --strings`, and friends). -## 6. Dependencies and FFI +On a 2026 tree I grepped, `src/` still had on the order of two hundred `unsafe` hits. One that stuck: BSD `getmntinfo` can return `0` with `NULL`; a wrapper that only rejected `len < 0` then called `from_raw_parts(null, 0)`. UB after a wrong libc check. -The claim already said: *no unsound `unsafe` in the crate **or its dependencies***. That clause needs its own diagram, because modern Rust is not `my code → rustc`. +When I started, I expected the interesting bugs to disappear. They did not. UAF and bounds bugs became much harder to write. The remaining pile moved toward FFI, races, swallowed `Result`s, and eventually the compiler. That is what the audit supports. It does not support “no CVEs” and it does not support “the rewrite was pointless.” -```mermaid -flowchart TB - M[Your crate] --> D[Direct crates.io deps] - D --> T[Transitive graph] - T --> U[Someone else's unsafe] - U --> F[FFI / C / kernel] - F --> OS[OS] -``` +## The compiler is in the threat model -`cargo audit` finds *known* advisories. It does not prove the graph is sound. I have stopped treating “our crate has no `unsafe`” as a complete sentence once `Cargo.lock` is in the picture. +Safe Rust’s theorem is only as strong as the compiler that implements it. Type system, rustc typeck, MIR, LLVM `noalias`, LLVM opts, backend — each stage can fail without the stage above being “wrong.” I mapped those IRs in the [pipeline piece](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). -FFI is the same story with a C ABI instead of a crate name. `CStr::from_ptr`, `from_raw_parts(ptr, len)`, `File::from_raw_fd` — the length came from libc. rustc never saw it. +I used to treat `noalias` as a backend curiosity. Then I watched what a dangling `&'static` *means* once typeck has blessed it: LLVM may treat that pointer as a real object and delete “impossible” loads. Memory safety is not the same as memory-model correctness. `&mut` is a uniqueness theorem. rustc lowers it to `noalias`. Stacked Borrows and Tree Borrows are the operational stories. If those stories disagree, “safe” code can be miscompiled. Miri can catch some of this. rustc + LLVM is what ships. -## 7. Case study: uutils +A pointer is not an integer. That is the sentence most Rust-vs-C threads skip. An implied-bounds hole is not a type-theory puzzle. It is a license for the optimizer. -One production data point, not a meta-analysis. +## ISSTA 2026 -Ubuntu 25.10 ships uutils (Rust coreutils). Canonical commissioned Zellic ahead of 26.04. The public write-up, [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/), is explicit: the CVE pile is TOCTOU, filesystem races, permission-after-create, GNU-parity logic, discarded `Result`s — including [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) (`dd` truncation swallowed with `Result::ok()`). The audit did **not** report buffer overflows, use-after-free, or uninitialized reads. GNU coreutils, over a comparable recent window, still shipped heap overwrites and OOB reads (`split --line-bytes`, `od --strings`, `unexpand --tabs`, `numfmt`). +Yusung Sim, Sukyoung Ryu (KAIST), Jaemin Hong (UNIST), *[Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i)*, ISSTA 2026. Artifact: [Zenodo](https://doi.org/10.5281/zenodo.20698055) (sheets restricted). -```mermaid -flowchart TB - A[uutils 2026 audit] --> B[Classic spatial / temporal memory] - A --> C[Semantic / system bugs] - B --> B1[largely absent] - C --> C1[TOCTOU / FFI length / logic / silent Result] -``` - -On a 2026 tree I grepped, `src/` still had on the order of two hundred `unsafe` keyword hits (libc, Win32, `mmap`, `from_raw_parts`). One that stuck: BSD `getmntinfo` can return `0` with `NULL`; a wrapper that only rejected `len < 0` then called `slice::from_raw_parts(null, 0)`. That is UB after a wrong libc check, not a missed borrow. - -When I started looking at Rust memory-safety claims, I expected the interesting bugs to disappear. They did not. The obvious UAF and bounds bugs became much harder to write. The remaining pile moved toward FFI, filesystem races, swallowed `Result`s, and — once I left application code — the compiler. That is what the audit actually supports. It does not support “Rust has no CVEs,” and it does not support “the rewrite was pointless.” - -## 8. The compiler boundary +I went looking for a measurement, not another anecdote. This is a study of buggy *type checking* — rustc accepted a program the rules should have rejected — not a study of buggy application crates. -Safe Rust’s theorem is only as strong as the compiler that implements it. The implementation is a pipeline, and each stage can fail independently. +A rustc crash is a reliability bug. Rejecting a valid program is a false reject. **Accepting an invalid program** is the soundness bug. Only the last one can launder a use-after-free through a green `cargo build`. Liu et al. (OOPSLA 2025) is the broader rustc-bug census. Sim, Ryu, and Hong specialize to accept-invalid and reconcile against Liu. -```mermaid -flowchart TB - T[Rust type system / spec] --> R[rustc typeck + borrowck] - R --> M[MIR transforms] - M --> L[LLVM IR + noalias] - L --> O[LLVM optimizations] - O --> B[Backend / codegen] - B --> H[Machine code] -``` +They crawled `A-*` typeck issues from Jan 2022–Sep 2025 (969), kept `C-bug` / `I-unsound` (320), then read them by hand (23). The conference abstract leads with 23. I almost cited that and stopped. The artifact then folds in 7 issues from Liu that pass the same bar. Analysis set: **30**. I wish the abstract had said that in one sentence. -| Layer | Possible failure | -|---|---| -| Type system / spec | Design hole (implied bounds, variance) | -| rustc typeck / borrowck | Soundness bug: accepts a program the spec forbids | -| MIR transformation | Invalid rewrite of a well-typed program | -| LLVM IR generation | Wrong `noalias`, wrong provenance, wrong ABI | -| LLVM optimization | Miscompile under aliasing rustc promised | -| Backend | Wrong machine code | -| FFI | Contract the IR cannot see | -| `unsafe` abstraction | Invalid invariant rustc was told to trust | +Five results, in the order they matter to me: -I used to treat `noalias` as a backend curiosity. Then I watched what a dangling `&'static` *means* once it has been blessed by typeck: LLVM is allowed to treat that pointer as a real object and delete “impossible” loads. Memory safety is not the same as memory-model correctness. `&mut` is a uniqueness theorem. rustc lowers that into LLVM `noalias`. Stacked Borrows and Tree Borrows are the operational stories of what those references may do. If the source-level story and the LLVM-level story disagree, “safe” code can be miscompiled, or `unsafe` that was careful under one model is UB under another. Miri can catch some of this. rustc + LLVM is what ships. +Some holes, typically implied bounds or trait objects, compromise memory safety. Sound typeck is strained by associated types and lifetimes-in-traits — not by `Vec` indexing. Most of these bugs were latent from the day the feature landed; #25860 (2015) is the extreme of that shape even though it sits outside their *report* window. Miri can see the subset that becomes a memory bug at run time; Chalk and a-mir-formality are not yet oracles. The Reference, FLS, and RFCs are often too vague to differential-test against. -A pointer is not an integer. That is the missing sentence in most Rust-vs-C threads. An implied-bounds hole is not a type-theory puzzle. It is a license for the optimizer. +Finding 1 is why the paper belongs in a memory-safety article. Finding 5 is why #25860 can sit open for a decade: if implied bounds plus variance are not an executable judgment, you cannot fail rustc with a spec test. You fail it with a program and a human argument. That is a slow test suite. -I mapped the IRs in the [rustc vs C++ pipeline piece](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). Below is the empirical evidence that typeck has, in fact, said yes to programs it should have rejected. +The [dev guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) already lists the family: #25860 (fn-pointer / variance), [#84591](https://github.com/rust-lang/rust/issues/84591) (HRTB supertrait), [#100051](https://github.com/rust-lang/rust/issues/100051) (projections in impl headers). Trait-object WF is the other long-running pile ([#44454](https://github.com/rust-lang/rust/issues/44454)). -## 9. Research: ISSTA 2026 rustc soundness study +Miri never sees rejected programs, so it is not a typeck oracle. C and C++ also lack a complete executable soundness spec. I am not scoring that as a unique humiliation. The difference is the *claim*. Rust’s slogan depends on typeck being sound. If the oracles cannot decide the edge, what you have is an engineering process — issue tracker, types team, next-gen solver — not a finished theorem. -Yusung Sim (KAIST), Sukyoung Ryu (KAIST), and Jaemin Hong (UNIST), *[Rust's Type Checker Implementation is Unsound: An Empirical Study on Soundness Bugs in rustc](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i)*, ISSTA 2026 (Oakland, 3–9 October 2026, co-located with SPLASH). Artifact: [Zenodo 10.5281/zenodo.20698055](https://doi.org/10.5281/zenodo.20698055) (analysis sheets for RQ1–RQ4; files restricted at time of writing). +I am not going to pretend everyday `HashMap` code is in this set. I am also not going to pretend the set is empty. The next section is the file I actually compiled. The artifact sheets were restricted; I did not invent medians the abstract does not state. -I went looking for a measurement, not another anecdote. This paper is that measurement. It is not a study of buggy Rust *programs*. It is a study of buggy *type checking*: rustc accepted a program the type rules should have rejected. +## #25860, on this machine -### 9.1 What they mean by a soundness bug +[#25860](https://github.com/rust-lang/rust/issues/25860) has been open since May 2015. The types team has treated a real fix as blocked on binders-with-where-clauses and the next-gen solver. [PR #156077](https://github.com/rust-lang/rust/pull/156077) (May 2026) was closed without landing; it did not bootstrap rustc. -Rust is marketed as a *type-sound* language: well-typed safe programs do not exhibit the undefined behaviors the type system is designed to rule out, including memory bugs. A **soundness bug in rustc** is narrower and more serious than an ICE or a false compile error: - -```text -program P should be rejected at typeck - │ - ▼ -rustc accepts P ← soundness bug (this paper) - │ - ▼ -P may then exhibit UB, a broken invariant, -or a memory bug — with no `unsafe` in P -``` - -A crash in rustc is a reliability bug. Rejecting a valid program is a completeness / false-reject bug. **Accepting an invalid program** is the soundness bug. Only the last one can launder a use-after-free through a green `cargo build`. - -Liu et al., *An Empirical Study of Bugs in the rustc Compiler* (OOPSLA 2025, [doi:10.1145/3763800](https://doi.org/10.1145/3763800)), is the broader rustc-bug census (crashes, miscompiles, false rejects). Sim, Ryu, and Hong *specialize* that space to type-soundness accept-invalid, and they explicitly reconcile their set against Liu et al. - -### 9.2 How the dataset was built - -Window: issues reported **1 January 2022 – 1 September 2025**, chosen to stay near recent rustc releases. - -```mermaid -flowchart LR - A["A-* typeck-related
969 issues"] --> B["C-bug or I-unsound
minus irrelevant labels
320"] - B --> C["Manual + dedup
23 soundness"] - C --> D["+ 7 from Liu et al.
that pass the same bar"] - D --> E["Final set
30"] -``` - -| Stage | Count | What it is | -|---|---|---| -| Crawl | 969 | rustc GitHub issues with area labels (`A-*`) tied to type checking | -| Label filter | 320 | Keep `C-bug` or `I-unsound`; drop labels that mark the issue as not a soundness report | -| Manual | 23 | Human read; duplicates removed; “is this actually accept-invalid?” | -| + Liu et al. | +7 of 18 unique-to-Liu | Same manual bar; **final N = 30** | - -I almost cited N=23 and stopped. That is what the conference abstract leads with. The artifact is explicit that they then folded in 7 issues from Liu et al. that passed the same manual bar, and the analysis set is 30. Both numbers are real. They measure different cuts. I use 30 as the analyzed set and 23 as the crawl-only set, and I wish the abstract had said that in one sentence. - -The artifact’s sheets answer **RQ1–RQ4** along the axes the paper names for every issue: - -| Axis | Question the authors ask | -|---|---| -| Affected feature | Which type-system feature is broken? | -| Symptom | *How* is it mishandled (wrong bound, skipped WF, bad coercion, …)? | -| Consequence | What undesirable behavior becomes possible? | -| Triggering features | Which *other* features must combine to fire it? | -| Community consensus | Did rustc maintainers treat it as a real soundness bug? | -| Lifecycle | When introduced, when found, when (if) fixed — Figure 1 in the paper | - -### 9.3 Five findings, read as a compiler threat model - -The abstract states five results. Mapped onto the safety stack in §2 and the pipeline in §8: - -| # | Finding | What it means for the slogan | -|---|---|---| -| 1 | Some soundness bugs, typically fired by **implied bounds** or **trait objects**, compromise **memory safety**. | Not every accept-invalid is a memory bug — but the ones that are, break the bumper sticker with no `unsafe` in the crate. | -| 2 | Sound typeck is strained by **associated types** and by **lifetimes interacting with traits**. | The hard region is not `Vec` indexing. It is the trait solver + region checker + well-formedness. | -| 3 | **Most bugs persist from the introduction of the feature** and take a long time to be discovered. | These are not “a bad weekend in 2024.” They are latent in the feature from day one. #25860 (2015) is the extreme of this shape, even though it sits outside their 2022–2025 *report* window. | -| 4 | **Miri** can detect the subset that becomes a memory bug at run time. **a-mir-formality** and **Chalk** are still too immature to serve as oracles for the other categories. | You cannot test rustc soundness by “running the program” unless the hole is observable as UB. Many holes are “wrong type accepted” with no immediate crash. | -| 5 | The **Rust Reference**, **FLS**, and **RFCs** often do not state the semantics precisely enough to be an oracle. | There is no complete, executable spec against which to differential-test rustc. The implementation *is* the spec more often than compiler engineers would like. | - -Finding 1 is why I put the paper in a memory-safety article at all. Finding 5 is why #25860 can sit open since 2015: if implied bounds plus variance are not an executable judgment, you cannot fail rustc with a spec test. You fail it with a program that should not compile and a human argument. That is a slow way to run a compiler test suite. - -**Implied bounds** (see the [rustc-dev-guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html)): from `&'a &'b T` the checker may assume `'b: 'a` without the user writing it. If that assumption is dropped under variance, fn-pointer reification, HRTB supertrait elaboration, or projection in an impl header, you get exactly the lifetime-laundering pattern in §10. Related open issues in the same family include [#100051](https://github.com/rust-lang/rust/issues/100051) (projections in impl headers) and [#84591](https://github.com/rust-lang/rust/issues/84591) (HRTB supertrait upcast). The guide itself lists those three as known unsoundnesses from implicit implied bounds. - -**Trait objects** (`dyn Trait`): well-formedness of the object type, lifetime bounds in the vtable, and “does `dyn Trait

: Trait

` imply the trait’s where-clauses?” have a long `I-unsound` history (e.g. [#44454](https://github.com/rust-lang/rust/issues/44454)). A hole here is not a style issue. It is a lie about what the vtable may be called with. - -**Associated types + lifetimes-in-traits**: projections can normalize on one path (call site) and stay unnormalized on another (impl checking), so an implied bound is assumed in the impl body and never proven at the use. That is a solver/WF bug, not an `unsafe` block. - -### 9.4 Oracles: what can actually test rustc? - -The paper’s second job is to ask whether existing artifacts could *find* these bugs, not just describe them after a human files GitHub issue #NNNNN. - -| Oracle | What it is | What the study reports | -|---|---|---| -| [Miri](https://github.com/rust-lang/miri) | MIR interpreter; Stacked/Tree Borrows | Useful **after** rustc accepts the program, if the hole is observable as UB / a memory bug. Not a typeck oracle: it never sees rejected programs. | -| [Chalk](https://github.com/rust-lang/chalk) | Logic-based trait solver (not production rustc) | Potential for trait-related holes; **immature** as a rustc soundness oracle. | -| [a-mir-formality](https://github.com/rust-lang/a-mir-formality) | Formal model of a Rust subset | Same: potential, not yet a drop-in differential test vs rustc. | -| Rust Reference | Informal language doc | Too imprecise to decide many edge cases. | -| [FLS](https://spec.ferrocene.dev/) (Ferrocene Language Specification) | Qualification-oriented spec | Same limit: not a complete executable type-soundness oracle. | -| RFCs | Design intent | Record what was *meant*; not what rustc *does* on the weird program. | - -C and C++ also lack a complete executable type-soundness spec, so I am not scoring this as a unique humiliation. The difference is the *claim*. Rust’s slogan depends on typeck being sound. If the oracles cannot decide the edge, what you have is an engineering process — issue tracker, types team, next-gen solver — not a finished theorem. - -```mermaid -flowchart TB - SPEC["Reference / FLS / RFCs
incomplete as oracles"] --> R[rustc typeck] - FORMAL["Chalk / a-mir-formality
immature"] -.->|cannot yet decide| R - R -->|accept-invalid| P[Well-typed according to rustc] - P --> MIRI[Miri] - MIRI -->|memory-bug subset| UB[UB / UAF observed] - MIRI -->|other soundness bugs| MISS[No crash — still unsound] -``` - -### 9.5 What this paper does *not* say - -- It does not say everyday `String` / `Vec` code is unsafe. The triggers are implied bounds, trait objects, associated types, lifetime–trait interaction. -- It does not measure how often these programs appear on crates.io. N=30 is a census of *reports*, not a prevalence study. -- It does not replace uutils. uutils is about production `unsafe` + TOCTOU. This paper is about rustc accepting *safe* programs it should reject. -- The full per-issue coding sheets live in a restricted artifact. Counts below the five abstract findings (exact split of memory-safety vs other consequences, median days-to-discovery) are not reproduced here as invented numbers. - -I am not going to pretend everyday `HashMap` code is in this set. I am also not going to pretend the set is empty. §10 is the one I actually compiled. - -## 10. Case study: rustc #25860 - -Ordinary application bugs live in *your* crate. Compiler soundness bugs live in *every* crate that hits the feature. - -| Category | Typical shape | -|---|---| -| Lifetimes / implied bounds | Nested references, outlives dropped under variance | -| Variance | Lifetime expansion through subtyping | -| HRTB | `for<'a>` fn-pointer / trait-bound coercion | -| Trait system | Associated types, projections that skip WF | -| Trait objects | Vtable / type-id assumptions | -| MIR | Invalid transform of a well-typed body | -| LLVM / codegen | Miscompile, wrong `noalias` | - -[#25860](https://github.com/rust-lang/rust/issues/25860) is the long-lived lifetime/variance case. Filed May 2015. Still open. The types team has treated a real fix as blocked on binders-with-where-clauses and the next-generation trait solver. [PR #156077](https://github.com/rust-lang/rust/pull/156077) (May 2026) was closed without landing; it did not bootstrap rustc. A June 2026 higher-ranked variant was closed as a duplicate. - -The `cve-rs` exploit uses **zero** `unsafe`. A sound helper +The `cve-rs` pattern uses **zero** `unsafe`. A sound helper ```rust fn lifetime_translator<'a, 'b, T: ?Sized>( @@ -481,118 +225,55 @@ pub fn as_static(x: &T) -> &'static T { } ``` -I compiled this on **rustc 1.93.1**. It accepted it. Zero `unsafe`. After dropping the `String` and allocating something the same size, debug aborted inside `ptr::copy_nonoverlapping`; release printed zeroes. That was the moment the “if it compiled, rustc proved it” sentence died for me — not as a claim about `Vec`, as a claim about rustc. - -Everyday application code does not look like HRTB fn-pointer coercion. If you lead a *tools* argument with this file, a competent reply is “compiler bug.” Fair. Lead with rustdoc search and compile wait if that is the argument. I am keeping the file here because I ran it, and because ISSTA 2026 is why I no longer treat it as a one-off. - -## 11. What moved, and what did not - -I already said this in the uutils section, so I will not dress it up again. Spatial and temporal memory bugs got much rarer in the safe subset. TOCTOU, GNU-parity logic, resource exhaustion, and FFI contracts did not. §3 is the lab version of the first half. uutils is the production version of the second. Waiting on rustc and not finding `replace` from a `String` search box is a third axis entirely — developer time, not CVEs. +I compiled this on **rustc 1.93.1**. It accepted it. After dropping the `String` and allocating something the same size, debug aborted inside `ptr::copy_nonoverlapping`; release printed zeroes. That was the moment “if it compiled, rustc proved it” died for me — not as a claim about `Vec`, as a claim about rustc. -## 12. Systems language, tools, and 2026 leftovers +Everyday application code does not look like HRTB fn-pointer coercion. If you lead a *tools* argument with this file, a competent reply is “compiler bug.” Fair. Lead with rustdoc search if that is the argument. I am keeping the file here because I ran it. -### 12.1 The 2023 thread, compressed +## Tools leftover from 2015 -[Why are some people against the Rust-Lang?](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) started from awkward `asm!`, missing protected-mode ops, and “Linux has apt-get, they didn’t add it.” ZiCog’s reply aged well: protected-mode code is a tiny kernel fraction; Linus saying Rust is “not altogether a bad idea” is, for Linus, praise; **Rust and C live together**; nobody is annotating a billion lines of old C. - -2026: Rust-for-Linux is in-tree, politically noisy, still mostly C. `asm!` is not the 2015 toy page; privileged ops still live in `.S` / `global_asm!` / a C stub, same as C kernels. kornel’s non-technical pile (anti-hype, C careers, “bugs are bad programmers”) still describes the internet. It does not decide whether `Vec` indexing is bounds-checked. - -### 12.2 2015 tools, scored without theology - -The useful mid-2010s post skipped the borrow checker and judged the tools. 2026: - -| Then | Now | Verdict | -|---|---|---| -| rustdoc: known unknowns; `replace` lives on `str` | `String` lists Deref methods; **search still ignores Deref** | Half-fixed. rust-analyzer is the real fix. | -| rustc is slow; future looks promising | Parallel frontend ~20–30% in tests, not default; Cranelift a few % | Still a tool defect. Promising for a decade. | -| Need type-based serialize | serde + derive | Solved. Fight is zero-copy. | -| Cannot ship apps | `cargo install`, [cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall) | Solved enough if you opt in. | -| Abstract return types | `impl Trait`, stable 1.26 | Solved. | -| Streaming / lending `Iterator` | GATs exist; **std has no `LendingIterator`** | Same hole. [rust-streaming](https://github.com/emk/rust-streaming). | -| `num` / const generics | `const N: usize` yes; `generic_const_exprs` incomplete | Mostly moved on. | - -I actually typed `replace` into rustdoc search on a `String` page. Deref methods are listed if you already know to scroll. Search still does not walk Deref. rust-analyzer does. That was the original hypothesis — rustdoc is good for known unknowns — and it is still true on the website. - -If you want to argue in *that* 2015 voice, those three leftovers are the argument. #25860 is a different argument. I keep mixing them in conversation; I am trying not to on the page. - -## 13. Where Rust wins, where C++ stays - -Do not ask “is Rust better than C++?” Ask **which component benefits from stronger invariants.** - -```mermaid -flowchart TB - SYS[Existing C++ system] - SYS --> UI[UI / application] - SYS --> BL[Business logic] - SYS --> NET[Networking] - SYS --> PAR[Parser] - SYS --> CORE[Memory-critical core] - SYS --> HW[Hardware / FFI / SDK] - CORE -.->|often| R[Rust candidate] - PAR -.->|often| R - NET -.->|sometimes| R - HW -.->|usually stay C/C++ or wrap| C[Keep C ABI] -``` +The useful mid-2010s post skipped the borrow checker and judged the tools. serde, `impl Trait` (1.26), `cargo install` / cargo-dist / cargo-binstall, and `const N: usize` mostly closed their original asks. Three did not. -I would reach for Rust on a new parser, a concurrent cache, anything where ownership is the actual problem and the C ABI surface is small enough to wrap. I would not reach for it as a moral upgrade of a 400 kLoC platform SDK wrapper, or a SIMD kernel that is already correct in C++ and paid for. Compile time is not a footnote on those teams. Hardware poke is not a footnote either. +I typed `replace` into rustdoc search on a `String` page. Deref methods are listed if you already know to scroll. Search still does not walk Deref. rust-analyzer does. That was the original hypothesis — rustdoc is good for known unknowns — and it is still true on the website. -### 13.1 Selective entry, not rewrite +rustc is still slow. Parallel frontend is a 2026 goal (~20–30% in tests, not default). Cranelift is a few percent. The future has looked promising for a decade. -“Rewrite it in Rust” is a meme I am tired of arguing with. New drivers, new Android native code, a sealed cache — those are plans. The [cache comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) is the piece I would send for one greenfield component. +`Iterator::Item` still cannot borrow from `&mut self`. GATs made a lending trait writable. std still does not have one. Zero-copy line parse is still crate-land ([rust-streaming](https://github.com/emk/rust-streaming)). -### 13.2 Could C++ adopt the ideas? +The May 2023 [users.rust-lang.org thread](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) started from awkward `asm!` and “Linux has apt-get.” ZiCog’s reply aged well: protected-mode code is a tiny kernel fraction; Rust and C live together; nobody is annotating a billion lines of old C. In 2026 Rust-for-Linux is in-tree and still mostly C. Privileged ops still live in `.S` files, same as C kernels. kornel’s pile (anti-hype, C careers, “bugs are bad programmers”) still describes the internet. It does not decide whether `Vec` indexing is bounds-checked. -It already adopted some: RAII, `unique_ptr` / `shared_ptr`, `std::span`, `string_view` (which also makes dangling easier to type), sanitizers, lifetime profiles, contracts experiments, Safe C++ / circle-style borrow checking. The remaining question is defaults: does C++ need Rust, or does C++ need the dangerous path to stop being the default? Both can be true. Sanitizers are opt-in and miss untested paths; rustc is opt-out for safe code and still has holes in rustc itself. +I keep mixing the tools leftovers with #25860 in conversation. They are different arguments. -## 14. 2026 scorecard +## Would I use it -| Claim you hear | Fair reading | -|---|---| -| Rust is memory safe. | **Safe** Rust is, modulo rustc soundness and unsound `unsafe` in the dep graph. | -| If it compiles, it cannot dangle. | False as an absolute. True for ordinary safe code that does not hit a compiler hole. ISSTA 2026: 30 rustc accept-invalid reports; #25860 is still open. | -| `unsafe` in `std` means the language is a con. | No. Encapsulation is the point. Unsound encapsulation is the bug. Count surface area. | -| Rust has no memory bugs in production. | Fewer *spatial/temporal* ones. Plenty of TOCTOU, logic, FFI-length. | -| Rust is not a systems language. | It is. It is not a drop-in for every privileged instruction or every C tree. | -| rustdoc and cargo solved onboarding. | rust-analyzer did more than rustdoc search. Compile time and lending iterators did not. | -| Rewrite it in Rust. | Usually a meme. New, sealed components are the realistic path. | +I would reach for Rust on a new parser, a concurrent cache, anything where ownership is the actual problem and the C ABI surface is small enough to wrap. I would not reach for it as a moral upgrade of a 400 kLoC SDK wrapper, or a SIMD kernel that is already correct in C++ and paid for. Compile time is not a footnote on those teams. -## 15. How to evaluate the next claim +“Rewrite it in Rust” is a meme I am tired of arguing with. New drivers, a sealed cache — those are plans. The [cache comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) is the piece I would send for one greenfield component. -When the next post says Rust “solved memory safety,” I now ask a smaller set of questions than I used to. Is this safe code or a kernel wrapper? Which rustc, and is the hole still open — ISSTA 2026 is the census for 2022–2025 reports; #25860 still is. Which bug class — a TOCTOU CVE does not refute a bounds check, and a GNU `split` heap overwrite does not vanish because Rust can panic. How much `unsafe` and FFI is actually in the tree. What did typeck, MIR, and `noalias` have to get right. +C++ already took pieces: RAII, smart pointers, `span`, sanitizers, lifetime profiles. `string_view` also made dangling easier to type. The remaining question is defaults. Sanitizers are opt-in and miss untested paths. rustc is opt-out for safe code and still has holes in rustc itself. -When the post says Rust is overhyped, I ask the reverse: did they show a safe, no-`unsafe`, not-a-compiler-bug UAF? The `transmute` snippet in §5 is not that demo. I compiled that one too. It is the escape hatch. +When the next post says Rust “solved memory safety,” I now ask: safe code or a kernel wrapper? which rustc? which bug class? how much `unsafe` is actually in the tree? When it says Rust is overhyped, I ask the reverse: did they show a safe, no-`unsafe`, not-a-compiler-bug UAF? The `transmute` snippet above is not that demo. I compiled that one too. It is the escape hatch. -## 16. Limits +## Limits -- UAF/OOB snippets: rustc 1.93.1 and gcc/g++ 13.3.0 on one machine; no sanitizers in the “compiles” column. -- #25860 checked on rustc 1.93.1; the tracking issue remains open. -- ISSTA 2026 details follow the [conference abstract](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) and [artifact record](https://doi.org/10.5281/zenodo.20698055). The per-issue coding sheets were restricted; this article does not invent splits or medians that the public abstract does not state. Abstract N=23 (crawl) vs artifact N=30 (crawl + Liu complement) are both reported. -- uutils remarks are from 2026 public audit writing, not a claim that every Rust CLI is clean. -- Compile-time and rustdoc-search behavior change release to release. -- This article does not measure serde vs rkyv, count in-tree Linux Rust drivers, or treat a soundness-hole SIGSEGV as evidence that typical application Rust is unsafe. +UAF/OOB snippets and #25860: rustc 1.93.1, gcc/g++ 13.3.0, one machine. #25860 is still open. ISSTA numbers follow the public abstract and artifact; I did not invent per-issue splits. uutils remarks are from public 2026 write-ups, not a claim that every Rust CLI is clean. rustdoc search and compile times move every release. -I keep coming back to the 2023 thread because it already had the stance I ended up with, before I had compiled anything: C and Rust can live together; programmer time is still the expensive input; compile-time checking is a bet that machines got cheaper faster than attention did. I just wanted the theorem written down, with the extra clauses visible. +I keep coming back to that 2023 thread because it already had the stance I ended up with, before I had compiled anything: C and Rust can live together; programmer time is still the expensive input; compile-time checking is a bet that machines got cheaper faster than attention did. I just wanted the extra clauses visible. -## 17. References +## References -1. [Why are some people against the Rust-Lang?](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906), users.rust-lang.org, May 2023. -2. [rust-lang/rust#25860](https://github.com/rust-lang/rust/issues/25860), implied bounds + variance (open since 2015). -3. [PR #156077](https://github.com/rust-lang/rust/pull/156077), attempted fix (closed May 2026, did not land). -4. [cve-rs](https://github.com/Speykious/cve-rs), lifetime expansion in safe Rust. -5. Yusung Sim, Sukyoung Ryu, Jaemin Hong, [Rust's Type Checker Implementation is Unsound: An Empirical Study on Soundness Bugs in rustc](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i), ISSTA 2026 (KAIST / UNIST). -6. Artifact, [10.5281/zenodo.20698055](https://doi.org/10.5281/zenodo.20698055) (RQ1–RQ4 sheets; 969 → 320 → 23 → 30). -7. Zixi Liu, Yang Feng, Yunbo Ni, Shaohua Li, Xizhe Yin, Qingkai Shi, Baowen Xu, Zhendong Su, [An Empirical Study of Bugs in the rustc Compiler](https://doi.org/10.1145/3763800), Proc. ACM Program. Lang. 9, OOPSLA2 (2025). -8. rustc-dev-guide, [Implied bounds](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) (lists #25860, #84591, #100051). -9. [Miri](https://github.com/rust-lang/miri), [Chalk](https://github.com/rust-lang/chalk), [a-mir-formality](https://github.com/rust-lang/a-mir-formality); [Ferrocene Language Specification](https://spec.ferrocene.dev/). -10. [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/), uutils / Canonical CVE set. -11. [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc), uutils `dd` / `Result::ok()`. -12. rustdoc book, [Search](https://doc.rust-lang.org/nightly/rustdoc/read-documentation/search.html) (Deref ignored). -13. [rust-lang/rust#19190](https://github.com/rust-lang/rust/issues/19190), rustdoc methods via auto-deref. -14. Rust project goals, [Promoting Parallel Front End (2026)](https://rust-lang.github.io/rust-project-goals/2026/parallel-front-end.html). -15. Nicholas Nethercote, [How to speed up the Rust compiler in July 2026](https://nnethercote.github.io/2026/07/31/how-to-speed-up-the-rust-compiler-in-july-2026.html). -16. [axodotdev/cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall). -17. [emk/rust-streaming](https://github.com/emk/rust-streaming). -18. Linus Walleij, [*Rust in Perspective*](https://people.kernel.org/linusw/rust-in-perspective). -19. Ralf Jung et al., Stacked Borrows; Tree Borrows (operational aliasing models; Miri). -20. [Rust vs Modern C++: Memory Safety Beyond the Hype](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype). -21. [Rustc Pipeline vs C++ Compilation Pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). +1. [Why are some people against the Rust-Lang?](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906), May 2023. +2. [rust-lang/rust#25860](https://github.com/rust-lang/rust/issues/25860). +3. [PR #156077](https://github.com/rust-lang/rust/pull/156077) (closed, did not land). +4. [cve-rs](https://github.com/Speykious/cve-rs). +5. Sim, Ryu, Hong, [Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i), ISSTA 2026. +6. Artifact [10.5281/zenodo.20698055](https://doi.org/10.5281/zenodo.20698055). +7. Liu et al., [Bugs in the rustc Compiler](https://doi.org/10.1145/3763800), OOPSLA 2025. +8. rustc-dev-guide, [Implied bounds](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html). +9. [Miri](https://github.com/rust-lang/miri), [Chalk](https://github.com/rust-lang/chalk), [a-mir-formality](https://github.com/rust-lang/a-mir-formality), [FLS](https://spec.ferrocene.dev/). +10. [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/). +11. [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc). +12. rustdoc [Search](https://doc.rust-lang.org/nightly/rustdoc/read-documentation/search.html); [#19190](https://github.com/rust-lang/rust/issues/19190). +13. [Parallel Front End (2026)](https://rust-lang.github.io/rust-project-goals/2026/parallel-front-end.html); Nethercote, [July 2026](https://nnethercote.github.io/2026/07/31/how-to-speed-up-the-rust-compiler-in-july-2026.html). +14. [cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), [rust-streaming](https://github.com/emk/rust-streaming). +15. Walleij, [*Rust in Perspective*](https://people.kernel.org/linusw/rust-in-perspective). +16. [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype); [Rustc pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). From f6f2d18709e0be39c7e1b86f015e959f727f5c65 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sat, 15 Aug 2026 10:56:33 +0530 Subject: [PATCH 03/14] Rewrite the Rust claims article in shorter, plainer English. Keep the same facts and demos; explain jargon on first use so the extra clauses on the slogan are easier to follow. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 255 ++++++++++++------- 1 file changed, 158 insertions(+), 97 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index 146006db..c588093f 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -1,6 +1,6 @@ --- title: "Rust Claims, a Reality Check: Safety, Tools, and Systems Programming" -description: "What 'Rust is memory safe' actually covers after compiling the demos, reading the uutils audit, and sitting with the ISSTA 2026 rustc soundness study." +description: "A plain-English look at what 'Rust is memory safe' really means: what the compiler stops, what it does not, and a real compiler bug." keywords: - rust memory safety - rustc soundness 25860 @@ -15,93 +15,123 @@ import TabItem from '@theme/TabItem'; import Head from '@docusaurus/Head'; - + # Rust Claims, a Reality Check: Safety, Tools, and Systems Programming :::note -Related: [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [Rustc vs C++ pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) +Related: [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [How rustc compiles vs C++](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) ::: -I started this the way I start most compiler arguments: take the slogan literally, then try to make rustc accept something it should not. +People say **Rust is memory safe**. I wanted to know what that actually means. So I compiled small programs, looked at a real Rust project, and read a 2026 paper about bugs in rustc (the Rust compiler). -**Rust is memory safe.** After a few evenings with rustc 1.93.1 and gcc 13.3, that sentence is still useful. It is also missing half its assumptions. I wanted the missing half on the page — not as a takedown, as the extra clauses I had to write down before I could defend the claim. +This is not “Rust is fake.” It is also not “Rust already fixed everything.” It is: the claim is real, but it is smaller than the short sentence on slides. -I also had to stop treating three fights as one. A 2015 tools rant (can you find `replace` from a `String` page? is rustc still slow?) is not a memory-safety argument. A 2023 “not a systems language” thread is not a borrow-checker argument. Mixing them is how comment sections stay loud. +There are three different fights online. Do not mix them. + +1. **Memory safety** — can the program smash memory? +2. **Tools** — is rustc slow? is the docs search bad? +3. **Systems work** — can you write a kernel, or only small apps? + +Those are different questions. ## Table of Contents -- [Where I landed](#where-i-landed) -- [The sentence I can actually defend](#the-sentence-i-can-actually-defend) -- [Which bugs are even in scope](#which-bugs-are-even-in-scope) -- [Four programs I compiled](#four-programs-i-compiled) -- [`unsafe`, deps, FFI](#unsafe-deps-ffi) -- [uutils](#uutils) -- [The compiler is in the threat model](#the-compiler-is-in-the-threat-model) -- [ISSTA 2026](#issta-2026) -- [#25860, on this machine](#25860-on-this-machine) -- [Tools leftover from 2015](#tools-leftover-from-2015) -- [Would I use it](#would-i-use-it) +- [The short answer](#the-short-answer) +- [What “memory safe” means here](#what-memory-safe-means-here) +- [What I compiled](#what-i-compiled) +- [The `unsafe` keyword](#the-unsafe-keyword) +- [Libraries and C code](#libraries-and-c-code) +- [A real project: uutils](#a-real-project-uutils) +- [The compiler can also be wrong](#the-compiler-can-also-be-wrong) +- [A 2026 research paper](#a-2026-research-paper) +- [Bug #25860, which I compiled](#bug-25860-which-i-compiled) +- [Old tool complaints, today](#old-tool-complaints-today) +- [Would I pick Rust?](#would-i-pick-rust) - [Limits](#limits) - [References](#references) -## Where I landed +## The short answer + +**Safe Rust** (code with no `unsafe` keyword) really does stop many memory bugs that C and C++ still allow. + +I could not make rustc 1.93.1 accept: + +- a pointer to a local `String` after the function ends +- writing `a[10]` on an array of size 4 -Safe Rust really does make use-after-free, spatial overflow, and data races on Rust-shared memory hard to write by accident. I could not get rustc to accept a dangling local or `a[10]` on a `[T; 4]`. gcc and g++ built both. +gcc and g++ 13.3 built both of those. -What I expected to vanish, and did not: TOCTOU, swallowed `Result`s, FFI length mistakes, and — once I stopped grepping application crates — rustc itself. [ISSTA 2026](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) is the census of that last layer. Miri helps *after* rustc already said yes. +Rust does **not** magically stop: -Android and Rust-for-Linux did not adopt a slogan. They adopted a default that moves one class of work onto the compiler. The compiler is a stack. That is the residue. +- logic bugs (the program does the wrong thing) +- file races (check a path, then someone changes the file) +- mistakes when talking to C +- bugs **inside rustc itself** -## The sentence I can actually defend +A 2026 paper (ISSTA) counted rustc bugs where the compiler said “ok” to code it should have rejected. Miri (a Rust checker) can catch some of those **after** rustc already accepted the code. -The slide says “Rust is memory safe.” After writing the extra clauses out: +Big companies use Rust because it moves one class of bugs to compile time. That is useful. The compiler is still software. Software has bugs. -> **Safe Rust**, compiled by a **sound rustc**, with **no unsound `unsafe` in the crate or its dependencies**, does not exhibit use-after-free, spatial buffer overflow, data races on shared memory, null dereference, or reads of uninitialized memory. +## What “memory safe” means here -Every extra clause is a place I later found a hole. The borrow checker can be doing its job and that sentence can still be false. +**Memory safety** means: the program only reads and writes memory it is allowed to use, and only while that memory is still alive. Two threads should not write the same memory at the same time with no lock. -## Which bugs are even in scope +It does **not** mean “the program is correct.” A program can be memory-safe and still delete the wrong file. -Memory safety here means loads and stores stay inside the object the language can name, for the lifetime it can prove, without a data race on that memory. It is not “the program does what you meant.” +Simple names: -| Bug class | Safe Rust | `unsafe` / FFI | C / C++ | +| Name | Plain meaning | +|---|---| +| Use-after-free (UAF) | Use memory after you freed it | +| Buffer overflow | Write past the end of an array | +| Double free | Free the same memory twice | +| Data race | Two threads touch the same memory in a bad way | +| Null | Use a pointer that is empty | +| Uninit | Read memory you never set | +| TOCTOU | Check a file, then it changes before you use it | +| FFI | Rust calling C (or C calling Rust) | +| Soundness bug | The compiler accepts code it should reject | + +| Kind of bug | Safe Rust | `unsafe` or C FFI | C / C++ | |---|---|---|---| -| UAF, spatial overflow, double-free, data race, null, uninit | Generally prevented | Possible | Possible | -| Integer overflow | Debug panic / release wrap — not C-style UB | Same | Often UB | -| TOCTOU, logic, resource exhaustion | Not in the theorem | Same | Same | -| FFI contract / compiler soundness | Not prevented | Possible | Possible | +| UAF, overflow, double-free, data race, null, uninit | Usually stopped | Possible | Possible | +| Integer wrap (numbers too big) | Debug: panic. Release: wrap. Not the same as C “undefined” smash | Same | Often dangerous | +| TOCTOU, logic bugs, out of memory | Not stopped | Not stopped | Not stopped | +| Bad C API / compiler bug | Not stopped | Possible | Possible | -A panic on `slice[i]` is the safe outcome. Continuing past a smashed canary is the other one. OOM abort, leaks, and deadlocks were never in the sentence above. +If you write `slice[i]` and `i` is too big, **safe Rust panics** (the program stops). That is the *safe* failure. In C the same index often corrupts memory and keeps running. -I used to stop the picture at the borrow checker. The first production crate I grepped (`unsafe`, `from_raw_parts`, `mmap`) made that feel silly. The hole can sit in rustc, in a dependency, or in a libc length, and the layer above is still “correct.” +I used to think “the borrow checker is the whole story.” Then I searched a real crate for `unsafe`. The hole can be in your `unsafe` block, in a library, in C, or in rustc. The layer above can still look fine. ```mermaid flowchart TB - P[program] --> S[safe Rust] - S --> BC[borrowck / typeck] - BC --> RC[rustc soundness] - RC --> DEP[deps + unsafe] - DEP --> FFI[FFI / kernel] - FFI --> HW[OS] + P[Your program] --> S[Safe Rust] + S --> BC[Compiler checks] + BC --> RC[Is rustc itself correct?] + RC --> DEP[Libraries + unsafe] + DEP --> FFI[C / OS] + FFI --> HW[Hardware] ``` -## Four programs I compiled +## What I compiled + +Same computer. **rustc 1.93.1**. **gcc/g++ 13.3**. Warnings on. No extra sanitizer tools unless I say so. -Same machine: **rustc 1.93.1**, **gcc/g++ 13.3.0**, `-Wall -Wextra`, no sanitizers unless I say so. Not a SPEC run. I only cared whether the frontend argued. +### 1. Use memory after free - + ```c char *p = malloc(32); free(p); -printf("%s", p); /* gcc: -Wuse-after-free, then a binary */ +printf("%s", p); /* gcc warns, then still makes a program */ ``` - + ```cpp auto* s = new std::string("secret"); @@ -111,7 +141,7 @@ std::cout << v; // g++ 13.3: no warning ``` - + ```rust fn dangling() -> &'static str { @@ -127,27 +157,38 @@ error[E0515]: cannot return reference to local variable `s` -The surprise was not rustc. It was g++: no diagnostic, binary on disk. gcc at least complained and then linked anyway. ASan would have caught both *if* I had turned it on. I did not, on purpose. The slogan is about the default build. +What surprised me was C++, not Rust. g++ made a binary and said nothing. gcc at least warned, then still linked. Tools like AddressSanitizer can catch the C/C++ bugs **if you turn them on**. I did not turn them on. The slogan is about the normal build, not the special test build. + +### 2. Write past the array -Constant `a[10]` on a four-element array: gcc/g++ still silent. rustc: +C and C++, array of 4, write index 10: **no warning**, program built. + +Rust: ```text error: this operation will panic at runtime a[10] = 42; -note: `#[deny(unconditional_panic)]` on by default ``` -A runtime `a[i]` in safe Rust still compiles and panics if `i` is hot. People paste that panic and call it a crash. I would rather have the panic than the smash gcc just emitted. +If the index is a **variable** (not the number 10 in the source), safe Rust still compiles. At run time it panics if the index is too big. Some people call that a crash. I would rather have a panic than silent memory corruption. + +### 3. File race (TOCTOU) -TOCTOU is still open. `std::fs` is path-shaped. The borrow checker does not see the inode. The 2026 uutils/Canonical CVE set is mostly that class. GNU coreutils has the same class *and* still ships spatial bugs. +You check a file path. Then someone swaps the file. Then you open the path. The compiler does not see that. Rust `std::fs` uses paths, like many C programs. The 2026 Ubuntu / uutils security review found many bugs of this kind. GNU coreutils has those too — **and** still has overflow bugs. -Past `extern "C"`, rustc is trusting a C ABI and a comment. Wrong `len`, a `NULL` the man page calls success, a truncated `mmap` — none of that is a missed borrow. If a talk only shows the first two tests, they showed the claim. The last two are where I spent the rest of the week. +### 4. Talking to C -Comment threads keep selling OOM abort, `mmap` SIGBUS, `File::from_raw_fd(stdin)` without `dup`, and `dd` allocating until the process dies as “so much for memory safety.” Those are real defects. They are not `strcpy` past a heap buffer. I am not trying to excuse them. I am trying not to count them twice. +When Rust calls C (`extern "C"`), rustc trusts the C side. Wrong length. A null pointer that the C docs call “success.” A memory map that another process shrinks. That is not the borrow checker failing. That is a contract with C. -## `unsafe`, deps, FFI +If a talk only shows tests 1 and 2, they showed the claim. Tests 3 and 4 are the rest of the story. -The first “gotcha” people sent me was this: +People also point at “out of memory, process dies” or “program panics” and say Rust is not safe. Those are not the same as `strcpy` past a buffer. They are still bugs. They are a different class. + +## The `unsafe` keyword + +`unsafe` means: “compiler, trust me here.” It is not a confession that Rust failed. It is the door out of the proof. + +This is the first trick people send: ```rust pub fn as_static(s: &str) -> &'static str { @@ -155,55 +196,73 @@ pub fn as_static(s: &str) -> &'static str { } ``` -No `unsafe` in `main`. Still UAF. That does not test the slogan. It tests whether rustc re-proves the body of every `unsafe` block at every call site. It does not. It trusts the signature. `std` is full of `unsafe` for the same reason: hide the dangerous bit. The interesting failure is when that hiding is a lie. +`main` has no `unsafe`. The program can still use memory after it is freed. Why? rustc checks the **function type**, not the proof inside `unsafe`. `std` uses `unsafe` too, on purpose: hide the dangerous bit. The bad case is when that hiding is a lie. + +Is a project with 500 `unsafe` blocks still safer than C? There is no yes/no. Two small `unsafe` blocks behind a clean API is the design. A crate that is basically C with Rust syntax is C with extra steps. I count, roughly: `unsafe` blocks, `unsafe fn`, `extern`, raw pointer tricks, and things rustc cannot see (file descriptors, `mmap`). That is not a science score. It is a way to talk in numbers. + +## Libraries and C code -“500 `unsafe` blocks, still safer than C?” is the wrong yes/no. I have seen a crate with two tiny blocks behind a boring API, and a crate that is libc with a Rust accent. Informally I count blocks, `unsafe fn`, `extern`, `from_raw_parts` / `transmute`, and invariants rustc cannot see (`mmap`, fds). Not a security metric. A way to stop arguing in the abstract. +“Our crate has no `unsafe`” is not the full story. Your `Cargo.lock` may pull in other crates that do. `cargo audit` finds **known** security reports. It does not prove every library is correct. -`cargo audit` finds known advisories. It does not prove `Cargo.lock` is sound. I have stopped treating “our crate has no `unsafe`” as a complete sentence. FFI is the same story with a C ABI instead of a crate name. The length came from libc. rustc never saw it. +Calling C is the same idea with a C API instead of a crate name. `from_raw_parts(pointer, length)` — the length came from C. rustc never checked it. -## uutils +## A real project: uutils -Ubuntu 25.10 ships the Rust coreutils. Canonical had Zellic look at them ahead of 26.04. [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) is explicit: the CVE pile is TOCTOU, filesystem races, GNU-parity logic, discarded `Result`s — including [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) (`dd` truncation swallowed with `.ok()`). They did **not** report buffer overflows, UAF, or uninitialized reads. GNU, over a comparable window, still shipped heap overwrites (`split --line-bytes`, `od --strings`, and friends). +**uutils** is GNU coreutils rewritten in Rust (`ls`, `dd`, `cp`, …). Ubuntu 25.10 ships it. Canonical paid a security firm (Zellic) to review it. -On a 2026 tree I grepped, `src/` still had on the order of two hundred `unsafe` hits. One that stuck: BSD `getmntinfo` can return `0` with `NULL`; a wrapper that only rejected `len < 0` then called `from_raw_parts(null, 0)`. UB after a wrong libc check. +The public write-up [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) says: the CVEs were mostly file races, permission bugs, “not the same as GNU,” and ignored errors. One example: [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) — `dd` hid a truncate error with `.ok()`. They did **not** report classic overflow / UAF. GNU, in a similar time window, still had heap overwrites. -When I started, I expected the interesting bugs to disappear. They did not. UAF and bounds bugs became much harder to write. The remaining pile moved toward FFI, races, swallowed `Result`s, and eventually the compiler. That is what the audit supports. It does not support “no CVEs” and it does not support “the rewrite was pointless.” +I searched a 2026 tree. About two hundred `unsafe` hits in `src/`. One bad case: a BSD C function can return length 0 and a null pointer. The Rust wrapper only rejected negative length, then built a slice from null. That is undefined behavior from a wrong C check. -## The compiler is in the threat model +I expected the “interesting” bugs to go away. They did not. Overflow and UAF got much harder. The remaining bugs moved to files, C, ignored `Result`s, and later the compiler. That is not “Rust has no CVEs.” It is also not “the rewrite was useless.” -Safe Rust’s theorem is only as strong as the compiler that implements it. Type system, rustc typeck, MIR, LLVM `noalias`, LLVM opts, backend — each stage can fail without the stage above being “wrong.” I mapped those IRs in the [pipeline piece](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). +## The compiler can also be wrong -I used to treat `noalias` as a backend curiosity. Then I watched what a dangling `&'static` *means* once typeck has blessed it: LLVM may treat that pointer as a real object and delete “impossible” loads. Memory safety is not the same as memory-model correctness. `&mut` is a uniqueness theorem. rustc lowers it to `noalias`. Stacked Borrows and Tree Borrows are the operational stories. If those stories disagree, “safe” code can be miscompiled. Miri can catch some of this. rustc + LLVM is what ships. +Safe Rust is only as strong as rustc. Code goes: types → rustc checks → MIR (Rust’s middle IR) → LLVM → machine code. Any step can fail. -A pointer is not an integer. That is the sentence most Rust-vs-C threads skip. An implied-bounds hole is not a type-theory puzzle. It is a license for the optimizer. +I used to ignore LLVM `noalias`. Then I saw what happens if rustc **wrongly** says a pointer lives forever (`&'static`). LLVM may treat that pointer as real and delete loads it thinks are impossible. **Memory safety** (don’t smash the heap) is not the same as **memory-model rules** (what the optimizer is allowed to assume). `&mut` means “only I can write.” rustc turns that into `noalias` for LLVM. If those two stories disagree, even “safe” code can be compiled wrong. -## ISSTA 2026 +A pointer is not “just a number.” Most internet fights skip that. A hole in lifetime rules is not a word game. It is permission for the optimizer. -Yusung Sim, Sukyoung Ryu (KAIST), Jaemin Hong (UNIST), *[Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i)*, ISSTA 2026. Artifact: [Zenodo](https://doi.org/10.5281/zenodo.20698055) (sheets restricted). +The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws those steps. -I went looking for a measurement, not another anecdote. This is a study of buggy *type checking* — rustc accepted a program the rules should have rejected — not a study of buggy application crates. +## A 2026 research paper -A rustc crash is a reliability bug. Rejecting a valid program is a false reject. **Accepting an invalid program** is the soundness bug. Only the last one can launder a use-after-free through a green `cargo build`. Liu et al. (OOPSLA 2025) is the broader rustc-bug census. Sim, Ryu, and Hong specialize to accept-invalid and reconcile against Liu. +Yusung Sim, Sukyoung Ryu (KAIST), Jaemin Hong (UNIST) wrote [Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) for ISSTA 2026. Extra files: [Zenodo](https://doi.org/10.5281/zenodo.20698055). -They crawled `A-*` typeck issues from Jan 2022–Sep 2025 (969), kept `C-bug` / `I-unsound` (320), then read them by hand (23). The conference abstract leads with 23. I almost cited that and stopped. The artifact then folds in 7 issues from Liu that pass the same bar. Analysis set: **30**. I wish the abstract had said that in one sentence. +This paper is **not** “Rust apps have bugs.” It is “rustc sometimes accepts programs it should reject.” -Five results, in the order they matter to me: +Three different compiler bugs: -Some holes, typically implied bounds or trait objects, compromise memory safety. Sound typeck is strained by associated types and lifetimes-in-traits — not by `Vec` indexing. Most of these bugs were latent from the day the feature landed; #25860 (2015) is the extreme of that shape even though it sits outside their *report* window. Miri can see the subset that becomes a memory bug at run time; Chalk and a-mir-formality are not yet oracles. The Reference, FLS, and RFCs are often too vague to differential-test against. +1. rustc **crashes** — annoying, not a memory smash in your app +2. rustc **rejects good code** — also annoying +3. rustc **accepts bad code** — this is the soundness bug. This one can hide a use-after-free behind `cargo build` with no `unsafe` -Finding 1 is why the paper belongs in a memory-safety article. Finding 5 is why #25860 can sit open for a decade: if implied bounds plus variance are not an executable judgment, you cannot fail rustc with a spec test. You fail it with a program and a human argument. That is a slow test suite. +Another paper (Liu et al., OOPSLA 2025) counted many kinds of rustc bugs. This ISSTA paper looks only at type (3), and compares with Liu. -The [dev guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) already lists the family: #25860 (fn-pointer / variance), [#84591](https://github.com/rust-lang/rust/issues/84591) (HRTB supertrait), [#100051](https://github.com/rust-lang/rust/issues/100051) (projections in impl headers). Trait-object WF is the other long-running pile ([#44454](https://github.com/rust-lang/rust/issues/44454)). +How they built the list: GitHub issues from Jan 2022 to Sep 2025 about types (969) → bug / unsound labels (320) → read by hand (**23**). The short abstract says 23. I almost stopped there. The extra files add 7 more from Liu. Final study set: **30**. I wish the abstract said both numbers. -Miri never sees rejected programs, so it is not a typeck oracle. C and C++ also lack a complete executable soundness spec. I am not scoring that as a unique humiliation. The difference is the *claim*. Rust’s slogan depends on typeck being sound. If the oracles cannot decide the edge, what you have is an engineering process — issue tracker, types team, next-gen solver — not a finished theorem. +What they found, in simple words: -I am not going to pretend everyday `HashMap` code is in this set. I am also not going to pretend the set is empty. The next section is the file I actually compiled. The artifact sheets were restricted; I did not invent medians the abstract does not state. +- Some of these bugs (often “implied bounds” or trait objects) can break memory safety. +- Hard cases are associated types and lifetimes mixed with traits — not `Vec` indexing. +- Many bugs were there from the day the feature shipped. Issue #25860 (2015) is the long example, even though it is older than their 2022–2025 window. +- **Miri** can catch the ones that blow up at run time. Other formal tools (Chalk, a-mir-formality) are not ready as a full test of rustc. +- The official docs are often not precise enough to use as an automatic test. -## #25860, on this machine +Why #25860 can stay open for years: if the rule is not written as a machine-checkable test, you cannot fail rustc with a spec. You fail it with a program plus a human saying “this should not compile.” That is slow. -[#25860](https://github.com/rust-lang/rust/issues/25860) has been open since May 2015. The types team has treated a real fix as blocked on binders-with-where-clauses and the next-gen solver. [PR #156077](https://github.com/rust-lang/rust/pull/156077) (May 2026) was closed without landing; it did not bootstrap rustc. +The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) already lists this family: #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). -The `cve-rs` pattern uses **zero** `unsafe`. A sound helper +C and C++ also do not have a full machine spec of “this must be rejected.” I am not picking on Rust for that. The difference is the **claim**. Rust’s short sentence needs rustc to be right. If tests cannot decide the edge, you have a team process (issues, types team, new solver), not a finished proof. + +Normal `HashMap` code is not this set. The set is also not empty. Next is the file I compiled. + +## Bug #25860, which I compiled + +[#25860](https://github.com/rust-lang/rust/issues/25860) has been open since May 2015. A real fix is waiting on bigger type-system work. [PR #156077](https://github.com/rust-lang/rust/pull/156077) in May 2026 was closed. It did not even build rustc. + +The [cve-rs](https://github.com/Speykious/cve-rs) example uses **zero** `unsafe`. A helper that is fine on its own: ```rust fn lifetime_translator<'a, 'b, T: ?Sized>( @@ -214,7 +273,7 @@ fn lifetime_translator<'a, 'b, T: ?Sized>( } ``` -is coerced to `for<'x> fn(_, &'x T) -> &'b T`. The implied `'b: 'a` is dropped. A `&&()` with `'static` then “proves” any lifetime: +gets copied as a function pointer in a way that drops a lifetime rule. Then a dummy `&&()` is used to pretend a short-lived value lives forever: ```rust const STATIC_UNIT: &&() = &&(); @@ -225,39 +284,41 @@ pub fn as_static(x: &T) -> &'static T { } ``` -I compiled this on **rustc 1.93.1**. It accepted it. After dropping the `String` and allocating something the same size, debug aborted inside `ptr::copy_nonoverlapping`; release printed zeroes. That was the moment “if it compiled, rustc proved it” died for me — not as a claim about `Vec`, as a claim about rustc. +I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. That is when “if it compiled, rustc proved it” died for me — not for normal `Vec` code, for rustc. + +Normal app code does not look like this. If you start a tools argument with this file, people will say “that is a compiler bug.” They are right. Start with docs search if that is your point. I keep this file because I ran it. -Everyday application code does not look like HRTB fn-pointer coercion. If you lead a *tools* argument with this file, a competent reply is “compiler bug.” Fair. Lead with rustdoc search if that is the argument. I am keeping the file here because I ran it. +## Old tool complaints, today -## Tools leftover from 2015 +Around 2015, some Rust users said: skip the borrow-checker fight, look at the tools. Some of that is fixed: serde, `impl Trait` (since 1.26), `cargo install` and cargo-dist, `const N: usize`. Three things are not. -The useful mid-2010s post skipped the borrow checker and judged the tools. serde, `impl Trait` (1.26), `cargo install` / cargo-dist / cargo-binstall, and `const N: usize` mostly closed their original asks. Three did not. +**Docs search.** I typed `replace` in rustdoc on the `String` page. The methods from `str` are listed if you scroll. Search still does not find them through `Deref`. rust-analyzer (the editor helper) does. The website is still weak for “I don’t know the name yet.” -I typed `replace` into rustdoc search on a `String` page. Deref methods are listed if you already know to scroll. Search still does not walk Deref. rust-analyzer does. That was the original hypothesis — rustdoc is good for known unknowns — and it is still true on the website. +**Compile time.** rustc is still slow. A parallel frontend is a 2026 goal (about 20–30% faster in tests, not the default yet). Small extra wins exist. People have said “the future looks good” for a long time. -rustc is still slow. Parallel frontend is a 2026 goal (~20–30% in tests, not default). Cranelift is a few percent. The future has looked promising for a decade. +**Streaming iterators.** A standard `Iterator` cannot yield a borrow from inside itself. You still cannot write, in std, a parser that hands out `&str` from its own buffer. Other crates exist ([rust-streaming](https://github.com/emk/rust-streaming)). -`Iterator::Item` still cannot borrow from `&mut self`. GATs made a lending trait writable. std still does not have one. Zero-copy line parse is still crate-land ([rust-streaming](https://github.com/emk/rust-streaming)). +A May 2023 [forum thread](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) asked why people dislike Rust. Some said inline assembly is awkward, or “Linux only has apt-get, they did not add Rust.” Fair replies: a tiny part of a kernel is special CPU instructions; Rust and C can live together; nobody will rewrite a billion lines of old C. In 2026, some Linux kernel code is Rust, most is still C. Special CPU ops still live in `.S` assembly files, like in C kernels. A lot of online hate is hype-backlash, not “`Vec` has no bounds check.” -The May 2023 [users.rust-lang.org thread](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) started from awkward `asm!` and “Linux has apt-get.” ZiCog’s reply aged well: protected-mode code is a tiny kernel fraction; Rust and C live together; nobody is annotating a billion lines of old C. In 2026 Rust-for-Linux is in-tree and still mostly C. Privileged ops still live in `.S` files, same as C kernels. kornel’s pile (anti-hype, C careers, “bugs are bad programmers”) still describes the internet. It does not decide whether `Vec` indexing is bounds-checked. +Docs search and compile wait are one argument. #25860 is another. I mix them when I talk. They are not the same. -I keep mixing the tools leftovers with #25860 in conversation. They are different arguments. +## Would I pick Rust? -## Would I use it +Yes, for new code where ownership is hard: a parser, a cache with threads, a small C API you can wrap. -I would reach for Rust on a new parser, a concurrent cache, anything where ownership is the actual problem and the C ABI surface is small enough to wrap. I would not reach for it as a moral upgrade of a 400 kLoC SDK wrapper, or a SIMD kernel that is already correct in C++ and paid for. Compile time is not a footnote on those teams. +No, as a “moral upgrade” of a huge old C/C++ SDK wrapper, or a math kernel that is already correct and fast in C++. Waiting on rustc is a real cost on those teams. -“Rewrite it in Rust” is a meme I am tired of arguing with. New drivers, a sealed cache — those are plans. The [cache comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) is the piece I would send for one greenfield component. +“Rewrite it in Rust” is usually a bad plan. New drivers or a new sealed component can be a plan. For one new cache, see the [Rust vs C++ comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype). -C++ already took pieces: RAII, smart pointers, `span`, sanitizers, lifetime profiles. `string_view` also made dangling easier to type. The remaining question is defaults. Sanitizers are opt-in and miss untested paths. rustc is opt-out for safe code and still has holes in rustc itself. +C++ already copied some ideas: RAII, smart pointers, `span`, sanitizers. `string_view` also made dangling pointers easier to type. The real question is the **default**. Sanitizers are extra flags and miss paths you never run. rustc checks safe code by default — and rustc still has holes. -When the next post says Rust “solved memory safety,” I now ask: safe code or a kernel wrapper? which rustc? which bug class? how much `unsafe` is actually in the tree? When it says Rust is overhyped, I ask the reverse: did they show a safe, no-`unsafe`, not-a-compiler-bug UAF? The `transmute` snippet above is not that demo. I compiled that one too. It is the escape hatch. +When someone says “Rust solved memory safety,” I now ask: safe code or kernel wrapper? which rustc? which kind of bug? how much `unsafe` is in the tree? When someone says “Rust is hype,” I ask: did they show a use-after-free with no `unsafe` and not a known compiler bug? The `transmute` snippet is not that demo. That is the escape hatch. I compiled that too. ## Limits -UAF/OOB snippets and #25860: rustc 1.93.1, gcc/g++ 13.3.0, one machine. #25860 is still open. ISSTA numbers follow the public abstract and artifact; I did not invent per-issue splits. uutils remarks are from public 2026 write-ups, not a claim that every Rust CLI is clean. rustdoc search and compile times move every release. +The small C/C++/Rust programs and #25860 were run on rustc 1.93.1 and gcc 13.3 on one machine. #25860 is still open. ISSTA numbers come from the public abstract and artifact; I did not invent extra stats. uutils notes come from public 2026 write-ups, not “every Rust CLI is clean.” Docs search and compile speed change every release. -I keep coming back to that 2023 thread because it already had the stance I ended up with, before I had compiled anything: C and Rust can live together; programmer time is still the expensive input; compile-time checking is a bet that machines got cheaper faster than attention did. I just wanted the extra clauses visible. +C and Rust can live together. People are still the expensive part. Checking more at compile time is a bet that computers got cheaper faster than human attention. I just wanted the extra words on the claim written down. ## References From 477d9b803690c427a2edef253f2ce7a921191994 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sat, 15 Aug 2026 13:25:47 +0530 Subject: [PATCH 04/14] Add compiler output, sanitizers, and a TOCTOU demo to the Rust claims article. The piece now walks through rustc vs gcc/g++ on the same bugs, what ASan/UBSan catch by default, and a check-then-open file example rustc does not see. --- docs/articles/rust-claims-a-reality-check.md | 686 +++++++++++++++++-- 1 file changed, 633 insertions(+), 53 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index c588093f..86f27bed 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -2,12 +2,54 @@ title: "Rust Claims, a Reality Check: Safety, Tools, and Systems Programming" description: "A plain-English look at what 'Rust is memory safe' really means: what the compiler stops, what it does not, and a real compiler bug." keywords: - - rust memory safety - - rustc soundness 25860 - - ISSTA 2026 rustc - - rust unsafe FFI - - rust lending iterator - - rust compile time + - Rust memory safety + - Rust safety guarantees + - Rust borrow checker + - Rust ownership + - Rust compiler + - rustc + - Rust compiler bugs + - Rust soundness + - Rust unsoundness + - Rust unsafe code + - Rust unsafe + - Rust FFI + - Rust memory bugs + - Rust use after free + - Rust buffer overflow + - Rust data races + - Rust lifetime + - Rust lifetimes + - Rust ownership system + - Rust borrowing + - Rust type system + - Rust compiler soundness + - rustc soundness + - rustc bug + - Rust compiler bug + - Rust security + - Rust systems programming + - Rust systems programming language + - Rust compile time safety + - Rust compile time checks + - Rust runtime safety + - Rust memory-safe language + - Rust safety limitations + - Rust unsafe FFI + - Rust lending iterator + - Rust iterator safety + - Rust compiler verification + - Rust static analysis + - Rust safety tools + - Rust sanitizers + - Rust Miri + - Rust Clippy + - Rust fuzzing + - Rust compiler fuzzing + - Rust bug discovery + - Rust soundness bugs + - ISSTA 2026 + - Rust ISSTA 2026 --- import Tabs from '@theme/Tabs'; @@ -28,11 +70,13 @@ People say **Rust is memory safe**. I wanted to know what that actually means. S This is not “Rust is fake.” It is also not “Rust already fixed everything.” It is: the claim is real, but it is smaller than the short sentence on slides. +What follows is the **top of the iceberg**: a few programs, a sanitizer flag, one rewrite (uutils), one rustc paper. Under that sit file races, C FFI, `unsafe` in libraries, optimizer rules, and bugs **inside rustc**. Those do not show up on the slide. They still ship. + There are three different fights online. Do not mix them. -1. **Memory safety** — can the program smash memory? -2. **Tools** — is rustc slow? is the docs search bad? -3. **Systems work** — can you write a kernel, or only small apps? +1. **Memory safety**: can the program smash memory? +2. **Tools**: is rustc slow? is the docs search bad? +3. **Systems work**: can you write a kernel, or only small apps? Those are different questions. @@ -41,6 +85,7 @@ Those are different questions. - [The short answer](#the-short-answer) - [What “memory safe” means here](#what-memory-safe-means-here) - [What I compiled](#what-i-compiled) +- [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates) - [The `unsafe` keyword](#the-unsafe-keyword) - [Libraries and C code](#libraries-and-c-code) - [A real project: uutils](#a-real-project-uutils) @@ -54,14 +99,194 @@ Those are different questions. ## The short answer -**Safe Rust** (code with no `unsafe` keyword) really does stop many memory bugs that C and C++ still allow. +**Safe Rust** (code with no `unsafe` keyword) really does stop many memory bugs that C and C++ still allow. C and C++ can catch some of the same bugs with [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer) / [UBSan](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html). Those flags are extra. rustc’s check on the examples below is the default. + +The argument I hear is real: + +- C and C++ still have memory bugs +- so they need a **stricter compiler**, or **language features** that make those bugs harder to type + +What each side actually offers: + +- **Rust:** both. Language rules cover two things rustc checks by **default**: + - **borrow checking**: who owns this memory, and whether a pointer to it is still valid + - **index checking**: the array has 4 slots; writing slot 10 is an error +- **C++:** + - language features you can pick: [`std::unique_ptr`](https://en.cppreference.com/w/cpp/memory/unique_ptr), [`std::span`](https://en.cppreference.com/w/cpp/container/span), [`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at). + - Plus a stricter compiler you can turn on: [ASan](https://github.com/google/sanitizers/wiki/AddressSanitizer) / [UBSan](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html). + +:::tip **The catch is the default.** +In `C` and `C++`, writing beyond the length of an array can still compile, but it may cause a `memory bug at runtime`. + +In `Rust`, the `compiler` usually catches this during `compilation time` ,hence the program can be `safe during runtme`. +::: + +A stricter compiler and new types do not delete human error. They move it. Split who is typing: + +- **C/C++ app developer**: the safer types exist, but they do not have to use them. They can still: + - index with `v[i]` instead of `v.at(i)` (no check) + - use `new` / `delete` instead of `unique_ptr` (easy to free too soon) + - `free(p)` then `printf("%s", p)` (use memory after it is gone) + - write `a[10]` on an array of size 4 (past the end) +- **Rust app developer**: can write [`unsafe`](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html), lie to the type system, or pass a bad length into C +- **gcc/g++ developer**: can miss a warning, ship bad codegen, or an optimizer bug +- **rustc developer**: can ship a [soundness bug](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) (accept code they should reject) + +Same species. Features only help if the app developer uses the safe default (Rust) or the safe API (C++ `v.at(i)`, not `v[i]`). + +The useful question is not who is smarter. It is **what each language gives you to find the mess**: + +- default rustc: language rules on +- default gcc/g++: those rules off +- C++ features you choose: `at()`, `span`, smart pointers +- sanitizers: if you pass the flag and run that path +- [Miri](https://github.com/rust-lang/miri): after rustc already said ok, for rustc holes + +Let’s look at examples I compiled with rustc 1.93.1 and gcc/g++ 13.3. Full output is in [What I compiled](#what-i-compiled) and [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates). + +**Example 1.** This function makes a `String` on the stack, then tries to hand a pointer to it back to the caller: + +```rust +fn dangling() -> &'static str { + let s = String::from("secret"); + &s +} +``` + +What it is doing: `s` lives only while `dangling` is running. When the function returns, `s` is destroyed. `&s` is a pointer into that dead memory. If rustc allowed this, the caller would read bytes that no longer belong to the program. + +What rustc did (default, no extra flag): + +```text +error[E0515]: cannot return reference to local variable `s` + --> uaf.rs:3:5 + | +3 | &s + | ^^ returns a reference to data owned by the current function +``` + +No binary. That reject is the good outcome. + +In C you can `free(p)` then `printf("%s", p)`: gcc warns `-Wuse-after-free` and still links. In C++ you can keep a `string_view` after `delete`: g++ 13.3 said nothing and still linked. Those programs can crash, print garbage, or read data an attacker put in the reused heap. + +Same C, with a sanitizer: + +```text +$ gcc -O0 -Wall -Wextra -fsanitize=address uaf.c -o uaf_c_asan +$ ./uaf_c_asan +ERROR: AddressSanitizer: heap-use-after-free +SUMMARY: AddressSanitizer: heap-use-after-free ... in printf_common +# abort, exit 1 +``` + +C++ `string_view` after `delete`, ASan: `heap-use-after-free` in `fwrite`, abort. So yes: **a sanitizer can report the same class of bug Rust refused.** You had to rebuild with `-fsanitize=address` and actually run `main`. rustc never let a binary out. + +**Example 2.** This program makes an array of four zeros, then writes index 10: + +```rust +fn main() { + let mut a = [0; 4]; + a[10] = 42; +} +``` + +What it is doing: valid indexes are 0, 1, 2, 3. Index 10 is six slots past the end. In C and C++ that write is undefined behavior: smash the stack, overwrite a return address, or look fine until it does not. gcc and g++ 13.3 with `-Wall -Wextra` built it with no diagnostic. -I could not make rustc 1.93.1 accept: +What rustc did (default): -- a pointer to a local `String` after the function ends -- writing `a[10]` on an array of size 4 +```text +error: this operation will panic at runtime + --> oob.rs:3:5 + | +3 | a[10] = 42; + | ^^^^^ index out of bounds: the length is 4 but the index is 10 + | + = note: `#[deny(unconditional_panic)]` on by default +``` + +No binary. -gcc and g++ 13.3 built both of those. +Same C with UBSan (ASan alone did not print a clean stack-overflow report on this tiny `int a[4]` in my run; UBSan did): + +```text +$ gcc -O0 -Wall -Wextra -fsanitize=undefined oob.c -o oob_ubsan +$ ./oob_ubsan +oob.c:3:6: runtime error: index 10 out of bounds for type 'int [4]' +``` + +Again: the sanitizer can name the same bug. Default gcc still shipped a binary. Default rustc did not. + +**Example 3.** Constant `a[10]` is the easy case. rustc can see the number. Now `i` comes from the command line, so the compiler cannot reject it up front. + +```rust +fn main() { + let mut a = [0i32; 4]; + let i: usize = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(10); + a[i] = 42; + println!("still running, a[0]={}", a[0]); +} +``` + +What it is doing: same four-slot array. The index is not in the source as `10`. rustc **accepts** this. You get a binary. The language still gives you a check: at run time. + +```text +$ rustc slice_i.rs -o slice_i_rs # exit 0 +$ ./slice_i_rs 4 +thread 'main' panicked at slice_i.rs:7:5: +index out of bounds: the len is 4 but the index is 4 +# exit 101: never prints "still running" +``` + +Same with `rustc -O`. Bounds checks stay in. Valid index `0` prints `still running, a[0]=42`. + +The C side, same idea: `i` from argv, array of 4, a neighbor `flag` sitting right after it: + +```c +struct Box { + int a[4]; + int flag; +}; +int i = atoi(argv[1]); /* we passed 4 */ +b.a[i] = 42; +printf("still running a[0]=%d flag=%d\n", b.a[0], b.flag); +``` + +```text +$ gcc -O0 -Wall -Wextra slice_i.c -o slice_i_c # exit 0, no warning +$ ./slice_i_c 4 +still running a[0]=0 flag=42 +# exit 0 +``` + +`flag` started as `7`. After `a[4] = 42` it is `42`. The program kept going. That is the unsafe failure. + +Now the sanitizer: this is the honest part. ASan + UBSan, **defaults**: + +```text +$ gcc -O0 -Wall -Wextra -fsanitize=address,undefined slice_i.c -o slice_i_san +$ ./slice_i_san 4 +slice_i.c:12:8: runtime error: index 4 out of bounds for type 'int [4]' +still running a[0]=0 flag=42 +# exit 0 +``` + +UBSan **printed** the same “index out of bounds” story Rust panics with. Then the program **kept running** and `flag` was still smashed. Default UBSan recovers. ASan did not stop this one: `a[4]` is the next field in the same `struct`, an intra-object overflow sanitizers often miss. + +If I add `-fno-sanitize-recover=undefined`, UBSan aborts and does not print `still running`. That flag is extra, like ASan is extra. Rust’s panic on `a[i]` needed no extra flag. + +**What each side actually gives you** + +| Where the human erred | What C/C++ gave me | What Rust gave me | +|---|---|---| +| Safe-looking UAF / constant overflow | Binary, unless I add ASan/UBSan and run that path | rustc error, no binary | +| Runtime `a[i]` too big | Default: smash and continue. Sanitizer: maybe a message; maybe still continue; maybe miss | Panic, default, debug and `-O` | +| `unsafe` / FFI / “trust me” | Same as C: you are on your own | rustc trusts you. [Miri](https://github.com/rust-lang/miri) can check **if** you run it | +| rustc itself wrong | (n/a) | ISSTA 2026: rustc accepted code it should reject. Miri after the fact | + +So: sanitizers can report what Rust reports. They are a tool you turn on. Safe Rust is a default. That is the real difference, not “Rust programmers never err.” They do. `unsafe` is that err. The language still marks the hole (`unsafe`) and still panics in the safe subset. C does not mark `a[i] = 42` as unsafe, and the default `a[i]` does not panic. Rust does **not** magically stop: @@ -70,7 +295,7 @@ Rust does **not** magically stop: - mistakes when talking to C - bugs **inside rustc itself** -A 2026 paper (ISSTA) counted rustc bugs where the compiler said “ok” to code it should have rejected. Miri (a Rust checker) can catch some of those **after** rustc already accepted the code. +A 2026 ISSTA paper, [Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) (Sim, Ryu, Hong; artifact on [Zenodo](https://doi.org/10.5281/zenodo.20698055)), counted rustc bugs where the compiler said “ok” to code it should have rejected. [Miri](https://github.com/rust-lang/miri) (a Rust checker) can catch some of those **after** rustc already accepted the code. That is Rust’s sanitizer-shaped answer for compiler holes: opt-in, after compile. Big companies use Rust because it moves one class of bugs to compile time. That is useful. The compiler is still software. Software has bugs. @@ -101,7 +326,7 @@ Simple names: | TOCTOU, logic bugs, out of memory | Not stopped | Not stopped | Not stopped | | Bad C API / compiler bug | Not stopped | Possible | Possible | -If you write `slice[i]` and `i` is too big, **safe Rust panics** (the program stops). That is the *safe* failure. In C the same index often corrupts memory and keeps running. +If you write `slice[i]` and `i` is too big, **safe Rust panics** (the program stops). That is the *safe* failure. In C the same index often corrupts memory and keeps running. [Example 3](#the-short-answer) is that test: Rust `./slice_i_rs 4` panics (exit 101). gcc `./slice_i_c 4` prints `still running` and `flag` changed from 7 to 42. I used to think “the borrow checker is the whole story.” Then I searched a real crate for `unsafe`. The hole can be in your `unsafe` block, in a library, in C, or in rustc. The layer above can still look fine. @@ -117,43 +342,102 @@ flowchart TB ## What I compiled -Same computer. **rustc 1.93.1**. **gcc/g++ 13.3**. Warnings on. No extra sanitizer tools unless I say so. +Same computer. **rustc 1.93.1** (`01f6ddf75`, 2026-02-11). **gcc/g++ 13.3.0**. Flags: `-Wall -Wextra` for C and C++. No AddressSanitizer. Commands: + +```bash +gcc -Wall -Wextra uaf.c -o uaf_c # exit 0 +g++ -std=c++20 -Wall -Wextra uaf.cpp -o uaf_cpp # exit 0 +rustc uaf.rs -o uaf_rs # error, no binary + +gcc -Wall -Wextra oob.c -o oob_c # exit 0 +g++ -std=c++20 -Wall -Wextra oob.cpp -o oob_cpp # exit 0 +rustc oob.rs -o oob_rs # error, no binary + +gcc -O0 -Wall -Wextra slice_i.c -o slice_i_c # exit 0 +rustc slice_i.rs -o slice_i_rs # exit 0 (index is a variable) +./slice_i_rs 4 # panic, exit 101 +./slice_i_c 4 # still running, flag smashed +``` ### 1. Use memory after free - + ```c -char *p = malloc(32); -free(p); -printf("%s", p); /* gcc warns, then still makes a program */ +#include +#include +int main(void) { + char *p = malloc(32); + if (!p) return 1; + free(p); + printf("%s", p); + return 0; +} ``` +```text +$ gcc -Wall -Wextra uaf.c -o uaf_c +uaf.c: In function ‘main’: +uaf.c:7:5: warning: pointer ‘p’ used after ‘free’ [-Wuse-after-free] + 7 | printf("%s", p); + | ^~~~~~~~~~~~~~~ +uaf.c:6:5: note: call to ‘free’ here + 6 | free(p); + | ^~~~~~~ +# exit code 0: you still get a binary +``` + +**Why that is bad.** `free` gave the heap block back. `printf` still reads it. The bytes may be garbage, may crash, or may be data an attacker put there after reuse. gcc saw the bug and **still linked**. A warning is not a stop. + - + ```cpp -auto* s = new std::string("secret"); -std::string_view v = *s; -delete s; -std::cout << v; // g++ 13.3: no warning +#include +#include +int main() { + auto* s = new std::string("secret"); + std::string_view v = *s; + delete s; + std::cout << v << "\n"; +} +``` + +```text +$ g++ -std=c++20 -Wall -Wextra uaf.cpp -o uaf_cpp +# no output +# exit code 0: binary produced ``` +**Why that is bad.** `string_view` is only a pointer plus length. After `delete s`, those bytes are dead. Printing `v` is use-after-free. g++ 13.3 did not even warn. You can ship this. + - + ```rust fn dangling() -> &'static str { let s = String::from("secret"); &s } +fn main() { println!("{}", dangling()); } ``` ```text +$ rustc uaf.rs -o uaf_rs error[E0515]: cannot return reference to local variable `s` + --> uaf.rs:3:5 + | +3 | &s + | ^^ returns a reference to data owned by the current function + +error: aborting due to 1 previous error ``` +**What rustc did.** `s` dies at the end of `dangling`. The `&s` would point at dead memory. rustc refused. **No object file, no binary.** + +**Why that reject is good.** You cannot run this program. You cannot put it in a release. The same class of bug that gcc warned-and-linked, and g++ silently linked, never leaves the compiler. That is the memory-safety claim in one command. + @@ -161,28 +445,321 @@ What surprised me was C++, not Rust. g++ made a binary and said nothing. gcc at ### 2. Write past the array -C and C++, array of 4, write index 10: **no warning**, program built. + + -Rust: +```c +int main(void) { + int a[4] = {0}; + a[10] = 42; + return a[10]; +} +``` ```text +$ gcc -Wall -Wextra oob.c -o oob_c +# no diagnostic +# exit code 0 +``` + +**Why that is bad.** The array has four `int`s. Index 10 is six slots past the end. In C that is undefined behavior: smash the stack, overwrite a return address, or “work” until it does not. gcc 13.3 with `-Wall -Wextra` still built it. + + + + +```cpp +int main() { + int a[4] = {0}; + a[10] = 42; + return a[10]; +} +``` + +```text +$ g++ -std=c++20 -Wall -Wextra oob.cpp -o oob_cpp +# no diagnostic +# exit code 0 +``` + +**Why that is bad.** Same write. Same undefined behavior. Same silent binary. + + + + +```rust +fn main() { + let mut a = [0; 4]; + a[10] = 42; +} +``` + +```text +$ rustc oob.rs -o oob_rs +warning: value assigned to `a` is never read + --> oob.rs:3:5 + | +3 | a[10] = 42; + | ^^^^^^^^^^ + error: this operation will panic at runtime - a[10] = 42; + --> oob.rs:3:5 + | +3 | a[10] = 42; + | ^^^^^ index out of bounds: the length is 4 but the index is 10 + | + = note: `#[deny(unconditional_panic)]` on by default + +error: aborting due to 1 previous error; 1 warning emitted +``` + +**What rustc did.** It saw length 4 and index 10 in the source. That write would always panic. The default `deny(unconditional_panic)` turns that into a **hard error**. Again: no binary. + +**Why that reject is good.** You never get a program that writes off the end of the array. In C that write can corrupt memory and keep running. rustc stops the constant case at compile time. + + + + +### 3. Variable index (`slice[i]`) + +rustc cannot reject this at compile time: `i` comes from `argv`. Full programs: + + + + +```c +#include +#include +struct Box { + int a[4]; + int flag; +}; +int main(int argc, char **argv) { + struct Box b; + b.a[0] = b.a[1] = b.a[2] = b.a[3] = 0; + b.flag = 7; + int i = argc > 1 ? atoi(argv[1]) : 4; + b.a[i] = 42; + printf("still running a[0]=%d flag=%d\n", b.a[0], b.flag); + return 0; +} +``` + +```text +$ gcc -O0 -Wall -Wextra slice_i.c -o slice_i_c +# no diagnostic, exit 0 +$ ./slice_i_c 4 +still running a[0]=0 flag=42 +# exit 0 +``` + +**Why that is bad.** Valid indexes of `a` are 0..3. Index 4 is the next `int`, which is `flag`. gcc wrote 42 into `flag` and continued. A bigger `i` (I tried 10) hit a segfault (exit 139). Either way: undefined behavior. The “lucky” case is the scary one: the program looks alive while it has already corrupted memory. + + + + +```rust +fn main() { + let mut a = [0i32; 4]; + let i: usize = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(10); + a[i] = 42; + println!("still running, a[0]={}", a[0]); +} +``` + +```text +$ rustc slice_i.rs -o slice_i_rs +# exit 0: unlike a[10] in the source, this is allowed +$ ./slice_i_rs 4 +thread 'main' panicked at slice_i.rs:7:5: +index out of bounds: the len is 4 but the index is 4 +# exit 101 +``` + +`rustc -O` panicked the same way. `./slice_i_rs 0` prints `still running, a[0]=42`. + +**What rustc did.** It could not prove `i` was in range, so it compiled a bounds check. At run time the check failed. The process stopped. It never printed `still running`. + +**Why that panic is good.** Some people call it a crash. It is the *safe* failure: no write past the array, no smashed `flag`. I would rather have a panic than silent corruption. + + + + +### 4. File race (TOCTOU) + +You check a file path. Then someone swaps the file. Then you open the path. The compiler does not see that. That bug has a name: [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use) (time-of-check to time-of-use). Rust [`std::fs`](https://doc.rust-lang.org/std/fs/) takes paths, like many C programs. Two syscalls on the same path string are two lookups. The file can change in between. + +Let’s look at a small program I compiled. It is the same shape as the CVEs, squeezed into one process so the window is visible. **Check** the path. **Swap** the file behind that name. **Use** the same path. rustc and gcc both accept it. ASan does not save you: this is not a memory smash. + +**What it is doing.** Write `target` with `hello`. Write `other` with `secret`. Call `metadata` / `stat` on `target` (the check). Replace `target` with `other`. Open `target` and read (the use). The check saw `hello`. The read got `secret`. + + + + +```rust +use std::fs::{self, File}; +use std::io::Read; + +fn main() { + let dir = std::env::temp_dir().join("cs_toctou_rs"); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("target"); + let other = dir.join("other"); + fs::write(&path, b"hello\n").unwrap(); + fs::write(&other, b"secret\n").unwrap(); + + let meta = fs::metadata(&path).unwrap(); + println!("check: is_file={} size={}", meta.is_file(), meta.len()); + + fs::remove_file(&path).unwrap(); + fs::rename(&other, &path).unwrap(); + + let mut s = String::new(); + File::open(&path).unwrap().read_to_string(&mut s).unwrap(); + println!("use: read {:?}", s.trim()); +} +``` + +```text +$ rustc toctou.rs -o toctou_rs +# exit 0: no error. The borrow checker does not see files. +$ ./toctou_rs +check: is_file=true size=6 +use: read "secret" +# exit 0 +``` + +**Why rustc is silent.** `path` is a string. Both calls are legal. Nothing was freed. No index was out of range. The bug is two lookups of one name. + + + + +```c +struct stat st; +stat(path, &st); /* check */ +printf("check: is_reg=%d size=%ld\n", S_ISREG(st.st_mode), (long)st.st_size); + +unlink(path); +rename(other, path); /* swap */ + +f = fopen(path, "r"); /* use */ +fgets(buf, sizeof buf, f); +printf("use: read \"%s\"\n", buf); +``` + +```text +$ gcc -Wall -Wextra toctou.c -o toctou_c +# exit 0 +$ ./toctou_c +check: is_reg=1 size=6 +use: read "secret" +# exit 0 + +$ gcc -O0 -Wall -Wextra -fsanitize=address toctou.c -o toctou_c_asan +$ ASAN_OPTIONS=detect_leaks=0 ./toctou_c_asan +check: is_reg=1 size=6 +use: read "secret" +# exit 0: ASan has nothing to say ``` -If the index is a **variable** (not the number 10 in the source), safe Rust still compiles. At run time it panics if the index is too big. Some people call that a crash. I would rather have a panic than silent memory corruption. + + + +In the real world the swap is another process in the gap between check and use, not `rename` in the same `main`. Same hole. The fix is not a smarter rustc. It is: open **once**, then `fstat` / operate on the **file descriptor**, or `O_NOFOLLOW`, so the name is not looked up twice. + +Canonical paid [Zellic](https://github.com/Zellic/publications) to audit Ubuntu’s Rust coreutils ([uutils](https://github.com/uutils/coreutils)). Write-up: [An update on rust-coreutils](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773) (22 Apr 2026). Report: [uutils coreutils: Zellic Audit Report](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf). CVE list: [oss-security](https://www.openwall.com/lists/oss-security/2026/05/02/2). Example: [CVE-2026-35359](https://www.openwall.com/lists/oss-security/2026/05/02/2): `cp` checks a path, then opens it without `O_NOFOLLOW`; an attacker can swap in a symlink. Ubuntu 26.04 still ships GNU `cp` / `mv` / `rm` because those races were still open. + +GNU coreutils still has **memory** bugs too. [CVE-2026-56392](https://osv.dev/vulnerability/CVE-2026-56392) (`unexpand`): integer wrap when sizing a buffer, then a heap overflow. [CERT Polska](https://cert.pl/en/posts/2026/07/CVE-2026-56391/) also lists [CVE-2026-56391](https://cert.pl/en/posts/2026/07/CVE-2026-56391/) (out-of-bounds read). So: uutils got TOCTOU CVEs; GNU still got overflow CVEs. Different class, both real. My test above is the TOCTOU class: rustc green, ASan green, wrong file. + +### 5. Talking to C + +When Rust calls C (`extern "C"`), rustc trusts the C side. That is [FFI](https://doc.rust-lang.org/nomicon/ffi.html) (foreign function interface). The language rules: [external blocks](https://doc.rust-lang.org/reference/items/external-blocks.html). Wrong length. A null pointer that the C docs call “success.” A memory map that another process shrinks. That is not the borrow checker failing. That is a contract with C. If you then do `from_raw_parts(ptr, len)`, rustc never measured `len`. + +If a talk only shows tests 1 and 2, they showed the *compile-time* claim. Test 3 is the *run-time* panic. Tests 4 and 5 are the rest of the story. + +People also point at “out of memory, process dies” or “program panics” and say Rust is not safe. Read the [panic docs](https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html). A panic unwinds or aborts. Allocation failure calls [`handle_alloc_error`](https://doc.rust-lang.org/std/alloc/fn.handle_alloc_error.html) (usually abort). Those are still bugs. They are not [`strcpy`](https://en.cppreference.com/w/c/string/byte/strcpy) past a buffer. `strcpy` is [undefined behavior](https://en.cppreference.com/w/c/language/behavior): the program may keep running on smashed memory. Panic stops. Different class. The [Rust reference](https://doc.rust-lang.org/reference/behavior-considered-undefined.html) lists what counts as UB in unsafe code. Safe indexing is not on that list; it panics instead. -### 3. File race (TOCTOU) +## C++23, sanitizers, Boost, crates -You check a file path. Then someone swaps the file. Then you open the path. The compiler does not see that. Rust `std::fs` uses paths, like many C programs. The 2026 Ubuntu / uutils security review found many bugs of this kind. GNU coreutils has those too — **and** still has overflow bugs. +The first tests used C arrays and `new`/`delete`. That is a fair “default C++ still lets you” demo. It is not a fair “C++ has no tools” demo. So I ran the same bugs again with **C++23**, **std::span**, **std::vector**, and [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer). Compiler: **g++ 13.3**, `-std=c++23`. Rust: **rustc 1.93.1** (current `stable` on this machine). I did not get a newer rustc; `rustup update` is how you would. + +**What C++23 did not change.** `std::span` is a pointer plus a length, like a Rust slice type, but `span[i]` does **not** check `i` in the default operator. I compiled this: + +```cpp +#include +#include +#include +int main(int argc, char** argv) { + std::vector v{0, 0, 0, 0}; + std::span s = v; + int i = argc > 1 ? std::stoi(argv[1]) : 4; + s[i] = 42; + std::cout << "still running s[0]=" << s[0] << "\n"; +} +``` + +```text +$ g++ -std=c++23 -O0 -Wall -Wextra cxx23_span.cpp -o cxx23_span +# exit 0, no warning +$ ./cxx23_span 4 +still running s[0]=0 +# exit 0 +``` + +New standard. Same silent write past the vector. C++23 is not a borrow checker. + +**What a sanitizer did change.** Same source, extra flag: + +```text +$ g++ -std=c++23 -O0 -Wall -Wextra -fsanitize=address cxx23_span.cpp -o cxx23_span_asan +$ ./cxx23_span_asan 4 +ERROR: AddressSanitizer: heap-buffer-overflow +WRITE of size 4 +SUMMARY: AddressSanitizer: heap-buffer-overflow ... in main +# abort, exit 1 +``` + +The `string_view` after `delete` program from test 1, still C++23: + +```text +$ g++ -std=c++23 -O0 -Wall -Wextra cxx23_uaf.cpp -o cxx23_uaf +$ ./cxx23_uaf +secret +# exit 0: printed freed memory + +$ g++ -std=c++23 -O0 -Wall -Wextra -fsanitize=address cxx23_uaf.cpp -o cxx23_uaf_asan +$ ./cxx23_uaf_asan +ERROR: AddressSanitizer: heap-use-after-free +SUMMARY: AddressSanitizer: heap-use-after-free ... fwrite +# abort, exit 1 +``` + +So: **C++23 + ASan caught both bugs at run time.** That is real. It is also optional. You must pass `-fsanitize=address`, take the slowdown, and **run the path**. ASan does not run on code you never execute. rustc’s check on `&s` and on `a[10]` happens at compile time with no extra flag. The variable-index panic happens on every run of that binary, debug or `-O`. + +**C++ already has a panic-shaped API.** [`std::vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) throws: + +```cpp +v.at(i) = 42; // i == 4 +``` + +```text +$ g++ -std=c++23 -O0 -Wall -Wextra cxx23_at.cpp -o cxx23_at +$ ./cxx23_at 4 +terminate called after throwing an instance of 'std::out_of_range' + what(): vector::_M_range_check: __n (which is 4) >= this->size() (which is 4) +# abort +``` -### 4. Talking to C +That is closer to Rust `v[i]`. The catch: the **usual** C++ index is `v[i]` / `span[i]`, which does not throw. Rust’s usual index is the checked one. Defaults matter. -When Rust calls C (`extern "C"`), rustc trusts the C side. Wrong length. A null pointer that the C docs call “success.” A memory map that another process shrinks. That is not the borrow checker failing. That is a contract with C. +**Boost vs crates.** C++ spent years putting Boost into `std`. [`boost::optional`](https://www.boost.org/doc/libs/release/libs/optional/doc/html/index.html) became [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional). Boost.Filesystem became [`std::filesystem`](https://en.cppreference.com/w/cpp/filesystem). Smart pointers, `span`, `string_view`: same story. This machine has Boost headers; I did not need them for the tests above because C++23 already has those types. What Boost still is: the leftover kitchen sink ([Asio](https://www.boost.org/doc/libs/release/doc/html/boost_asio.html), Spirit, uBLAS, …) until `std` or another library eats it. -If a talk only shows tests 1 and 2, they showed the claim. Tests 3 and 4 are the rest of the story. +Rust does **not** need a Boost-the-project. [`std`](https://doc.rust-lang.org/std/) already has `Option`, `Result`, `Box` / `Rc` / `Arc`, `Vec`, slices, [`std::fs`](https://doc.rust-lang.org/std/fs/). The Boost-sized rest lives on [crates.io](https://crates.io/): [Tokio](https://tokio.rs/) is Asio, [serde](https://serde.rs/) is serialization, [nix](https://docs.rs/nix) is POSIX, [nom](https://docs.rs/nom) is parsers. Cargo pulls one version and records it in `Cargo.lock`. That is why “is there a Boost for Rust?” is the wrong question. There is an ecosystem. The cost is the same cost Boost always had: you must trust the crate, same as you must trust a Boost module or a C++ library. -People also point at “out of memory, process dies” or “program panics” and say Rust is not safe. Those are not the same as `strcpy` past a buffer. They are still bugs. They are a different class. +None of that makes `span[i]` check bounds by default. Libraries do not replace the compiler’s default. ## The `unsafe` keyword @@ -204,13 +781,13 @@ Is a project with 500 `unsafe` blocks still safer than C? There is no yes/no. Tw “Our crate has no `unsafe`” is not the full story. Your `Cargo.lock` may pull in other crates that do. `cargo audit` finds **known** security reports. It does not prove every library is correct. -Calling C is the same idea with a C API instead of a crate name. `from_raw_parts(pointer, length)` — the length came from C. rustc never checked it. +Calling C is the same idea with a C API instead of a crate name. `from_raw_parts(pointer, length)`: the length came from C. rustc never checked it. ## A real project: uutils **uutils** is GNU coreutils rewritten in Rust (`ls`, `dd`, `cp`, …). Ubuntu 25.10 ships it. Canonical paid a security firm (Zellic) to review it. -The public write-up [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) says: the CVEs were mostly file races, permission bugs, “not the same as GNU,” and ignored errors. One example: [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) — `dd` hid a truncate error with `.ok()`. They did **not** report classic overflow / UAF. GNU, in a similar time window, still had heap overwrites. +The public write-up [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) says: the CVEs were mostly file races, permission bugs, “not the same as GNU,” and ignored errors. One example: [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc): `dd` hid a truncate error with `.ok()`. They did **not** report classic overflow / UAF. GNU, in a similar time window, still had heap overwrites. I searched a 2026 tree. About two hundred `unsafe` hits in `src/`. One bad case: a BSD C function can return length 0 and a null pointer. The Rust wrapper only rejected negative length, then built a slice from null. That is undefined behavior from a wrong C check. @@ -234,9 +811,9 @@ This paper is **not** “Rust apps have bugs.” It is “rustc sometimes accept Three different compiler bugs: -1. rustc **crashes** — annoying, not a memory smash in your app -2. rustc **rejects good code** — also annoying -3. rustc **accepts bad code** — this is the soundness bug. This one can hide a use-after-free behind `cargo build` with no `unsafe` +1. rustc **crashes**: annoying, not a memory smash in your app +2. rustc **rejects good code**: also annoying +3. rustc **accepts bad code**: this is the soundness bug. This one can hide a use-after-free behind `cargo build` with no `unsafe` Another paper (Liu et al., OOPSLA 2025) counted many kinds of rustc bugs. This ISSTA paper looks only at type (3), and compares with Liu. @@ -245,9 +822,9 @@ How they built the list: GitHub issues from Jan 2022 to Sep 2025 about types (96 What they found, in simple words: - Some of these bugs (often “implied bounds” or trait objects) can break memory safety. -- Hard cases are associated types and lifetimes mixed with traits — not `Vec` indexing. +- Hard cases are associated types and lifetimes mixed with traits: not `Vec` indexing. - Many bugs were there from the day the feature shipped. Issue #25860 (2015) is the long example, even though it is older than their 2022–2025 window. -- **Miri** can catch the ones that blow up at run time. Other formal tools (Chalk, a-mir-formality) are not ready as a full test of rustc. +- **[Miri](https://github.com/rust-lang/miri)** can catch the ones that blow up at run time. Other formal tools (Chalk, a-mir-formality) are not ready as a full test of rustc. - The official docs are often not precise enough to use as an automatic test. Why #25860 can stay open for years: if the rule is not written as a machine-checkable test, you cannot fail rustc with a spec. You fail it with a program plus a human saying “this should not compile.” That is slow. @@ -284,7 +861,7 @@ pub fn as_static(x: &T) -> &'static T { } ``` -I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. That is when “if it compiled, rustc proved it” died for me — not for normal `Vec` code, for rustc. +I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. That is when “if it compiled, rustc proved it” died for me. Not for normal `Vec` code. For rustc. Normal app code does not look like this. If you start a tools argument with this file, people will say “that is a compiler bug.” They are right. Start with docs search if that is your point. I keep this file because I ran it. @@ -310,13 +887,13 @@ No, as a “moral upgrade” of a huge old C/C++ SDK wrapper, or a math kernel t “Rewrite it in Rust” is usually a bad plan. New drivers or a new sealed component can be a plan. For one new cache, see the [Rust vs C++ comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype). -C++ already copied some ideas: RAII, smart pointers, `span`, sanitizers. `string_view` also made dangling pointers easier to type. The real question is the **default**. Sanitizers are extra flags and miss paths you never run. rustc checks safe code by default — and rustc still has holes. +C++ already copied some ideas: RAII, smart pointers, `span`, sanitizers. `string_view` also made dangling pointers easier to type. The real question is the **default**. Sanitizers are extra flags and miss paths you never run. rustc checks safe code by default. rustc still has holes. When someone says “Rust solved memory safety,” I now ask: safe code or kernel wrapper? which rustc? which kind of bug? how much `unsafe` is in the tree? When someone says “Rust is hype,” I ask: did they show a use-after-free with no `unsafe` and not a known compiler bug? The `transmute` snippet is not that demo. That is the escape hatch. I compiled that too. ## Limits -The small C/C++/Rust programs and #25860 were run on rustc 1.93.1 and gcc 13.3 on one machine. #25860 is still open. ISSTA numbers come from the public abstract and artifact; I did not invent extra stats. uutils notes come from public 2026 write-ups, not “every Rust CLI is clean.” Docs search and compile speed change every release. +The small C/C++/Rust programs and #25860 were run on rustc 1.93.1 and gcc/g++ 13.3 on one machine. C++23 tests used `-std=c++23`; ASan used `-fsanitize=address`. #25860 is still open. ISSTA numbers come from the public abstract and artifact; I did not invent extra stats. uutils notes come from Canonical’s 2026 post, the Zellic PDF, and oss-security, not “every Rust CLI is clean.” Docs search and compile speed change every release. C and Rust can live together. People are still the expensive part. Checking more at compile time is a bet that computers got cheaper faster than human attention. I just wanted the extra words on the claim written down. @@ -332,9 +909,12 @@ C and Rust can live together. People are still the expensive part. Checking more 8. rustc-dev-guide, [Implied bounds](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html). 9. [Miri](https://github.com/rust-lang/miri), [Chalk](https://github.com/rust-lang/chalk), [a-mir-formality](https://github.com/rust-lang/a-mir-formality), [FLS](https://spec.ferrocene.dev/). 10. [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/). -11. [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc). -12. rustdoc [Search](https://doc.rust-lang.org/nightly/rustdoc/read-documentation/search.html); [#19190](https://github.com/rust-lang/rust/issues/19190). -13. [Parallel Front End (2026)](https://rust-lang.github.io/rust-project-goals/2026/parallel-front-end.html); Nethercote, [July 2026](https://nnethercote.github.io/2026/07/31/how-to-speed-up-the-rust-compiler-in-july-2026.html). -14. [cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), [rust-streaming](https://github.com/emk/rust-streaming). -15. Walleij, [*Rust in Perspective*](https://people.kernel.org/linusw/rust-in-perspective). -16. [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype); [Rustc pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). +11. Canonical, [An update on rust-coreutils](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773); [Zellic audit PDF](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf); [oss-security CVE list](https://www.openwall.com/lists/oss-security/2026/05/02/2). +12. [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc). [CVE-2026-56392](https://osv.dev/vulnerability/CVE-2026-56392); [CERT Polska on GNU coreutils](https://cert.pl/en/posts/2026/07/CVE-2026-56391/). +13. [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use); Rust [`std::fs`](https://doc.rust-lang.org/std/fs/); [FFI](https://doc.rust-lang.org/nomicon/ffi.html); [panic](https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html); [`strcpy`](https://en.cppreference.com/w/c/string/byte/strcpy); [undefined behavior (C)](https://en.cppreference.com/w/c/language/behavior). +14. [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer); [UBSan](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html); [`std::span`](https://en.cppreference.com/w/cpp/container/span); [`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at); [Boost](https://www.boost.org/); [crates.io](https://crates.io/). +15. rustdoc [Search](https://doc.rust-lang.org/nightly/rustdoc/read-documentation/search.html); [#19190](https://github.com/rust-lang/rust/issues/19190). +16. [Parallel Front End (2026)](https://rust-lang.github.io/rust-project-goals/2026/parallel-front-end.html); Nethercote, [July 2026](https://nnethercote.github.io/2026/07/31/how-to-speed-up-the-rust-compiler-in-july-2026.html). +17. [cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), [rust-streaming](https://github.com/emk/rust-streaming). +18. Walleij, [*Rust in Perspective*](https://people.kernel.org/linusw/rust-in-perspective). +19. [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype); [Rustc pipeline](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline). From 6b724a1575e606ed2dbb4b99a5cc553abe250f10 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 09:46:53 +0530 Subject: [PATCH 05/14] Add clang 18 results next to gcc on the Rust claims examples. Clang still ships the UAF and TOCTOU programs; it warns on a constant a[10] and links anyway. ASan/UBSan match gcc when the flags are on. --- docs/articles/rust-claims-a-reality-check.md | 111 ++++++++++++++----- 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index 86f27bed..a1f7589f 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -129,7 +129,7 @@ A stricter compiler and new types do not delete human error. They move it. Split - `free(p)` then `printf("%s", p)` (use memory after it is gone) - write `a[10]` on an array of size 4 (past the end) - **Rust app developer**: can write [`unsafe`](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html), lie to the type system, or pass a bad length into C -- **gcc/g++ developer**: can miss a warning, ship bad codegen, or an optimizer bug +- **gcc / g++ / clang developer**: can miss a warning, ship bad codegen, or an optimizer bug - **rustc developer**: can ship a [soundness bug](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) (accept code they should reject) Same species. Features only help if the app developer uses the safe default (Rust) or the safe API (C++ `v.at(i)`, not `v[i]`). @@ -137,12 +137,12 @@ Same species. Features only help if the app developer uses the safe default (Rus The useful question is not who is smarter. It is **what each language gives you to find the mess**: - default rustc: language rules on -- default gcc/g++: those rules off +- default gcc / g++ / clang: those rules off - C++ features you choose: `at()`, `span`, smart pointers - sanitizers: if you pass the flag and run that path - [Miri](https://github.com/rust-lang/miri): after rustc already said ok, for rustc holes -Let’s look at examples I compiled with rustc 1.93.1 and gcc/g++ 13.3. Full output is in [What I compiled](#what-i-compiled) and [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates). +Let’s look at examples I compiled with rustc 1.93.1, gcc/g++ 13.3, and **clang/clang++ 18.1.3**. Full output is in [What I compiled](#what-i-compiled) and [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates). **Example 1.** This function makes a `String` on the stack, then tries to hand a pointer to it back to the caller: @@ -167,7 +167,7 @@ error[E0515]: cannot return reference to local variable `s` No binary. That reject is the good outcome. -In C you can `free(p)` then `printf("%s", p)`: gcc warns `-Wuse-after-free` and still links. In C++ you can keep a `string_view` after `delete`: g++ 13.3 said nothing and still linked. Those programs can crash, print garbage, or read data an attacker put in the reused heap. +In C you can `free(p)` then `printf("%s", p)`: gcc warns `-Wuse-after-free` and still links. **clang 18.1.3** with `-Wall -Wextra` said **nothing** and still linked. In C++ you can keep a `string_view` after `delete`: g++ 13.3 and clang++ 18 both said nothing and still linked. Those programs can crash, print garbage, or read data an attacker put in the reused heap. Same C, with a sanitizer: @@ -177,6 +177,11 @@ $ ./uaf_c_asan ERROR: AddressSanitizer: heap-use-after-free SUMMARY: AddressSanitizer: heap-use-after-free ... in printf_common # abort, exit 1 + +$ clang -O0 -Wall -Wextra -fsanitize=address uaf.c -o uaf_clang_asan +$ ./uaf_clang_asan +ERROR: AddressSanitizer: heap-use-after-free +SUMMARY: AddressSanitizer: heap-use-after-free ... in printf_common ``` C++ `string_view` after `delete`, ASan: `heap-use-after-free` in `fwrite`, abort. So yes: **a sanitizer can report the same class of bug Rust refused.** You had to rebuild with `-fsanitize=address` and actually run `main`. rustc never let a binary out. @@ -190,7 +195,7 @@ fn main() { } ``` -What it is doing: valid indexes are 0, 1, 2, 3. Index 10 is six slots past the end. In C and C++ that write is undefined behavior: smash the stack, overwrite a return address, or look fine until it does not. gcc and g++ 13.3 with `-Wall -Wextra` built it with no diagnostic. +What it is doing: valid indexes are 0, 1, 2, 3. Index 10 is six slots past the end. In C and C++ that write is undefined behavior: smash the stack, overwrite a return address, or look fine until it does not. gcc and g++ 13.3 with `-Wall -Wextra` built it with **no diagnostic**. clang and clang++ 18 warned `-Warray-bounds` and **still linked**. What rustc did (default): @@ -212,6 +217,10 @@ Same C with UBSan (ASan alone did not print a clean stack-overflow report on thi $ gcc -O0 -Wall -Wextra -fsanitize=undefined oob.c -o oob_ubsan $ ./oob_ubsan oob.c:3:6: runtime error: index 10 out of bounds for type 'int [4]' + +$ clang -O0 -Wall -Wextra -fsanitize=undefined oob.c -o oob_clang_ubsan +# also -Warray-bounds at compile time, then: +oob.c:3:5: runtime error: index 10 out of bounds for type 'int[4]' ``` Again: the sanitizer can name the same bug. Default gcc still shipped a binary. Default rustc did not. @@ -259,6 +268,11 @@ $ gcc -O0 -Wall -Wextra slice_i.c -o slice_i_c # exit 0, no warning $ ./slice_i_c 4 still running a[0]=0 flag=42 # exit 0 + +$ clang -O0 -Wall -Wextra slice_i.c -o slice_i_clang +$ ./slice_i_clang 4 +still running a[0]=0 flag=42 +# exit 0: same smash ``` `flag` started as `7`. After `a[4] = 42` it is `42`. The program kept going. That is the unsafe failure. @@ -271,6 +285,13 @@ $ ./slice_i_san 4 slice_i.c:12:8: runtime error: index 4 out of bounds for type 'int [4]' still running a[0]=0 flag=42 # exit 0 + +$ clang -O0 -Wall -Wextra -fsanitize=address,undefined slice_i.c -o slice_i_clang_san +$ ./slice_i_clang_san 4 +slice_i.c:12:5: runtime error: index 4 out of bounds for type 'int[4]' +SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ... +still running a[0]=0 flag=42 +# exit 0 ``` UBSan **printed** the same “index out of bounds” story Rust panics with. Then the program **kept running** and `flag` was still smashed. Default UBSan recovers. ASan did not stop this one: `a[4]` is the next field in the same `struct`, an intra-object overflow sanitizers often miss. @@ -342,21 +363,21 @@ flowchart TB ## What I compiled -Same computer. **rustc 1.93.1** (`01f6ddf75`, 2026-02-11). **gcc/g++ 13.3.0**. Flags: `-Wall -Wextra` for C and C++. No AddressSanitizer. Commands: +Same computer. **rustc 1.93.1** (`01f6ddf75`, 2026-02-11). **gcc/g++ 13.3.0**. **clang/clang++ 18.1.3**. Flags: `-Wall -Wextra` for C and C++. No AddressSanitizer unless I say so. Commands: ```bash -gcc -Wall -Wextra uaf.c -o uaf_c # exit 0 -g++ -std=c++20 -Wall -Wextra uaf.cpp -o uaf_cpp # exit 0 -rustc uaf.rs -o uaf_rs # error, no binary +gcc -Wall -Wextra uaf.c -o uaf_c # exit 0, warns -Wuse-after-free +clang -Wall -Wextra uaf.c -o uaf_clang # exit 0, no warning +g++ -std=c++20 -Wall -Wextra uaf.cpp -o uaf_cpp # exit 0, silent +clang++ -std=c++20 -Wall -Wextra uaf.cpp -o uaf_clangpp # exit 0, silent +rustc uaf.rs -o uaf_rs # error, no binary -gcc -Wall -Wextra oob.c -o oob_c # exit 0 -g++ -std=c++20 -Wall -Wextra oob.cpp -o oob_cpp # exit 0 -rustc oob.rs -o oob_rs # error, no binary +gcc -Wall -Wextra oob.c -o oob_c # exit 0, no diagnostic +clang -Wall -Wextra oob.c -o oob_clang # exit 0, -Warray-bounds, still a binary +rustc oob.rs -o oob_rs # error, no binary -gcc -O0 -Wall -Wextra slice_i.c -o slice_i_c # exit 0 -rustc slice_i.rs -o slice_i_rs # exit 0 (index is a variable) -./slice_i_rs 4 # panic, exit 101 -./slice_i_c 4 # still running, flag smashed +clang -O0 -Wall -Wextra slice_i.c -o slice_i_clang +./slice_i_clang 4 # still running, flag smashed ``` ### 1. Use memory after free @@ -388,7 +409,13 @@ uaf.c:6:5: note: call to ‘free’ here # exit code 0: you still get a binary ``` -**Why that is bad.** `free` gave the heap block back. `printf` still reads it. The bytes may be garbage, may crash, or may be data an attacker put there after reuse. gcc saw the bug and **still linked**. A warning is not a stop. +```text +$ clang -Wall -Wextra uaf.c -o uaf_clang +# no diagnostic +# exit code 0 +``` + +**Why that is bad.** `free` gave the heap block back. `printf` still reads it. The bytes may be garbage, may crash, or may be data an attacker put there after reuse. gcc saw the bug and **still linked**. clang 18 with `-Wall -Wextra` did not even warn. A warning is not a stop. @@ -406,11 +433,11 @@ int main() { ```text $ g++ -std=c++20 -Wall -Wextra uaf.cpp -o uaf_cpp -# no output -# exit code 0: binary produced +$ clang++ -std=c++20 -Wall -Wextra uaf.cpp -o uaf_clangpp +# both: no output, exit 0, binary produced ``` -**Why that is bad.** `string_view` is only a pointer plus length. After `delete s`, those bytes are dead. Printing `v` is use-after-free. g++ 13.3 did not even warn. You can ship this. +**Why that is bad.** `string_view` is only a pointer plus length. After `delete s`, those bytes are dead. Printing `v` is use-after-free. g++ 13.3 and clang++ 18 did not even warn. You can ship this. @@ -436,12 +463,12 @@ error: aborting due to 1 previous error **What rustc did.** `s` dies at the end of `dangling`. The `&s` would point at dead memory. rustc refused. **No object file, no binary.** -**Why that reject is good.** You cannot run this program. You cannot put it in a release. The same class of bug that gcc warned-and-linked, and g++ silently linked, never leaves the compiler. That is the memory-safety claim in one command. +**Why that reject is good.** You cannot run this program. You cannot put it in a release. The same class of bug that gcc warned-and-linked, clang silently linked, and g++/clang++ silently linked, never leaves rustc. That is the memory-safety claim in one command. -What surprised me was C++, not Rust. g++ made a binary and said nothing. gcc at least warned, then still linked. Tools like AddressSanitizer can catch the C/C++ bugs **if you turn them on**. I did not turn them on. The slogan is about the normal build, not the special test build. +What surprised me was C++, not Rust. g++ and clang++ made a binary and said nothing. gcc at least warned, then still linked. clang 18 did not warn on the `free` then `printf` case. Tools like AddressSanitizer can catch the C/C++ bugs **if you turn them on**. I did not turn them on for the default builds. The slogan is about the normal build, not the special test build. ### 2. Write past the array @@ -460,9 +487,15 @@ int main(void) { $ gcc -Wall -Wextra oob.c -o oob_c # no diagnostic # exit code 0 + +$ clang -Wall -Wextra oob.c -o oob_clang +oob.c:3:5: warning: array index 10 is past the end of the array (that has type 'int[4]') [-Warray-bounds] + 3 | a[10] = 42; + | ^ ~~ +# exit code 0: you still get a binary ``` -**Why that is bad.** The array has four `int`s. Index 10 is six slots past the end. In C that is undefined behavior: smash the stack, overwrite a return address, or “work” until it does not. gcc 13.3 with `-Wall -Wextra` still built it. +**Why that is bad.** The array has four `int`s. Index 10 is six slots past the end. In C that is undefined behavior: smash the stack, overwrite a return address, or “work” until it does not. gcc 13.3 with `-Wall -Wextra` still built it with no warning. clang 18 warned, then **still linked**. rustc refused. @@ -477,11 +510,13 @@ int main() { ```text $ g++ -std=c++20 -Wall -Wextra oob.cpp -o oob_cpp -# no diagnostic -# exit code 0 +# no diagnostic, exit 0 + +$ clang++ -std=c++20 -Wall -Wextra oob.cpp -o oob_clangpp +# -Warray-bounds, exit 0, binary produced ``` -**Why that is bad.** Same write. Same undefined behavior. Same silent binary. +**Why that is bad.** Same write. Same undefined behavior. gcc silent. clang warns. Both ship a binary. @@ -663,6 +698,16 @@ $ ASAN_OPTIONS=detect_leaks=0 ./toctou_c_asan check: is_reg=1 size=6 use: read "secret" # exit 0: ASan has nothing to say + +$ clang -Wall -Wextra toctou.c -o toctou_clang +$ ./toctou_clang +check: is_reg=1 size=6 +use: read "secret" +$ clang -O0 -Wall -Wextra -fsanitize=address toctou.c -o toctou_clang_asan +$ ASAN_OPTIONS=detect_leaks=0 ./toctou_clang_asan +check: is_reg=1 size=6 +use: read "secret" +# exit 0 ``` @@ -684,7 +729,7 @@ People also point at “out of memory, process dies” or “program panics” a ## C++23, sanitizers, Boost, crates -The first tests used C arrays and `new`/`delete`. That is a fair “default C++ still lets you” demo. It is not a fair “C++ has no tools” demo. So I ran the same bugs again with **C++23**, **std::span**, **std::vector**, and [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer). Compiler: **g++ 13.3**, `-std=c++23`. Rust: **rustc 1.93.1** (current `stable` on this machine). I did not get a newer rustc; `rustup update` is how you would. +The first tests used C arrays and `new`/`delete`. That is a fair “default C++ still lets you” demo. It is not a fair “C++ has no tools” demo. So I ran the same bugs again with **C++23**, **std::span**, **std::vector**, and [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer). Compilers: **g++ 13.3** and **clang++ 18.1.3**, `-std=c++23`. Rust: **rustc 1.93.1**. **What C++23 did not change.** `std::span` is a pointer plus a length, like a Rust slice type, but `span[i]` does **not** check `i` in the default operator. I compiled this: @@ -703,9 +748,12 @@ int main(int argc, char** argv) { ```text $ g++ -std=c++23 -O0 -Wall -Wextra cxx23_span.cpp -o cxx23_span -# exit 0, no warning +$ clang++ -std=c++23 -O0 -Wall -Wextra cxx23_span.cpp -o cxx23_span_clang +# both: exit 0, no warning $ ./cxx23_span 4 still running s[0]=0 +$ ./cxx23_span_clang 4 +still running s[0]=0 v.size()=4 # exit 0 ``` @@ -720,6 +768,11 @@ ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 4 SUMMARY: AddressSanitizer: heap-buffer-overflow ... in main # abort, exit 1 + +$ clang++ -std=c++23 -O0 -Wall -Wextra -fsanitize=address cxx23_span.cpp -o cxx23_span_clang_asan +$ ./cxx23_span_clang_asan 4 +ERROR: AddressSanitizer: heap-buffer-overflow +SUMMARY: AddressSanitizer: heap-buffer-overflow ... in main ``` The `string_view` after `delete` program from test 1, still C++23: @@ -893,7 +946,7 @@ When someone says “Rust solved memory safety,” I now ask: safe code or kerne ## Limits -The small C/C++/Rust programs and #25860 were run on rustc 1.93.1 and gcc/g++ 13.3 on one machine. C++23 tests used `-std=c++23`; ASan used `-fsanitize=address`. #25860 is still open. ISSTA numbers come from the public abstract and artifact; I did not invent extra stats. uutils notes come from Canonical’s 2026 post, the Zellic PDF, and oss-security, not “every Rust CLI is clean.” Docs search and compile speed change every release. +The small C/C++/Rust programs and #25860 were run on rustc 1.93.1, gcc/g++ 13.3, and clang/clang++ 18.1.3 on one machine. C++23 tests used `-std=c++23`; ASan used `-fsanitize=address`. #25860 is still open. ISSTA numbers come from the public abstract and artifact; I did not invent extra stats. uutils notes come from Canonical’s 2026 post, the Zellic PDF, and oss-security, not “every Rust CLI is clean.” Docs search and compile speed change every release. C and Rust can live together. People are still the expensive part. Checking more at compile time is a bet that computers got cheaper faster than human attention. I just wanted the extra words on the claim written down. From 2ddc255ca44b873e351ac03d96c30eb76cd87eca Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 09:57:34 +0530 Subject: [PATCH 06/14] Give the Rust claims article a framework: three safety levels, a same-bug trio, and CVE walkthroughs. Adds the C/C++/Rust scoreboard, what the language does and does not guarantee, safety vs correctness, the rustc pipeline question, cost of the checks, and a decision table. --- docs/articles/rust-claims-a-reality-check.md | 242 +++++++++++++++++-- 1 file changed, 222 insertions(+), 20 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index a1f7589f..c77c22be 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -83,16 +83,20 @@ Those are different questions. ## Table of Contents - [The short answer](#the-short-answer) +- [Three levels of Rust safety](#three-levels-of-rust-safety) - [What “memory safe” means here](#what-memory-safe-means-here) +- [The same bug in C, C++, and Rust](#the-same-bug-in-c-c-and-rust) +- [What Rust guarantees](#what-rust-guarantees) +- [Safety is not correctness](#safety-is-not-correctness) - [What I compiled](#what-i-compiled) - [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates) - [The `unsafe` keyword](#the-unsafe-keyword) - [Libraries and C code](#libraries-and-c-code) -- [A real project: uutils](#a-real-project-uutils) +- [A real CVE, two ways](#a-real-cve-two-ways) - [The compiler can also be wrong](#the-compiler-can-also-be-wrong) - [A 2026 research paper](#a-2026-research-paper) - [Bug #25860, which I compiled](#bug-25860-which-i-compiled) -- [Old tool complaints, today](#old-tool-complaints-today) +- [What you pay for the checks](#what-you-pay-for-the-checks) - [Would I pick Rust?](#would-i-pick-rust) - [Limits](#limits) - [References](#references) @@ -320,6 +324,30 @@ A 2026 ISSTA paper, [Rust's Type Checker Implementation is Unsound](https://conf Big companies use Rust because it moves one class of bugs to compile time. That is useful. The compiler is still software. Software has bugs. +## Three levels of Rust safety + +The slide says “Rust.” That is three different products glued together. + +**Level 1: safe Rust.** No `unsafe` in *your* function. rustc checks who owns the memory, how long a pointer is valid, and (for `a[i]`) whether the index fits. That is the claim people mean. + +**Level 2: `unsafe`.** You told rustc to trust you. The type still looks safe to callers. The proof is now a human. + +**Level 3: FFI + the toolchain.** C libraries, ABI, file descriptors, `mmap`, rustc itself, LLVM. The language rules stop at the `extern "C"` door. They also stop if rustc is wrong. + +```text + Rust safety + | + +----------+----------+ + | | | + Safe Rust unsafe FFI / toolchain + | | | + ownership you hold C, ABI, + borrowing the proof rustc, LLVM + index checks +``` + +The rest of this article is that picture filled in with programs. + ## What “memory safe” means here **Memory safety** means: the program only reads and writes memory it is allowed to use, and only while that memory is still alive. Two threads should not write the same memory at the same time with no lock. @@ -349,6 +377,116 @@ Simple names: If you write `slice[i]` and `i` is too big, **safe Rust panics** (the program stops). That is the *safe* failure. In C the same index often corrupts memory and keeps running. [Example 3](#the-short-answer) is that test: Rust `./slice_i_rs 4` panics (exit 101). gcc `./slice_i_c 4` prints `still running` and `flag` changed from 7 to 42. +One table for the experiments above (safe Rust, **default** C/C++ build, no sanitizer): + +| Bug / property | C | C++ | Safe Rust | +|---|---|---|---| +| Out-of-bounds **constant** index (`a[10]` on size 4) | Compiles (gcc silent; clang warns, still links) | Compiles | Compile error | +| Out-of-bounds **runtime** index | Undefined behavior; my run smashed a neighbor `flag` | Same with `span[i]` | Panic; process stops | +| Use-after-free | Possible; gcc may warn | Possible; `string_view` was silent | Ownership: no binary | +| Data race | Possible | Possible | Prevented in safe code | +| Null dereference | Possible | Possible | `Option` / references, not raw null | +| Manual `free` / `delete` | Yes | Yes | Usually unnecessary (`Box` / `Vec` drop it) | + +That is the takeaway. Sanitizers and `vector::at` move C++ closer to the right-hand column. They are not the default. + +## The same bug in C, C++, and Rust + +One hole, three spellings. Allocate an `int`, free it, write through the old pointer. + + + + +```c +int *p = malloc(sizeof(int)); +free(p); +*p = 42; /* gcc: -Wuse-after-free, then links */ +``` + + + + +```cpp +int *p = new int; +delete p; +*p = 42; /* g++ / clang++: still a program */ +``` + + + + +```rust +fn main() { + let mut p = Box::new(10); + drop(p); + *p = 42; +} +``` + +```text +$ rustc drop.rs +error[E0382]: use of moved value: `p` + --> drop.rs:4:5 + | +2 | let mut p = Box::new(10); + | ----- move occurs because `p` has type `Box` +3 | drop(p); + | - value moved here +4 | *p = 42; + | ^^^^^^^ value used here after move +``` + + + + +**Why rustc rejects this**, not “because Rust is safer” as a slogan. `Box` **owns** the heap `i32`. `drop(p)` **moves** that owner into `drop`. After the move, `p` is gone. There is no pointer left to write. C `free(p)` only marks the heap free; the **variable** `p` is still a number you can store through. That is the language rule, not a smarter programmer. + +The longer `String` / `string_view` demos in [What I compiled](#what-i-compiled) are the same rule with more bytes. + +## What Rust guarantees + +Rust’s memory-safety claim applies to **safe Rust**, if rustc is sound, and if you did not smuggle a lie through `unsafe` or C. It does **not** mean every Rust program is free of bugs. + +**Safe Rust is built to stop** + +- use-after-free, double-free, dangling references +- out-of-bounds access (compile error if rustc can see it; panic if `i` is a variable) +- data races (two threads, same memory, no lock, at least one write) +- using `null` as a value: you use `Option` instead + +**Safe Rust does not automatically stop** + +- logic bugs (wrong algorithm, wrong result) +- races that are **not** data races (TOCTOU, check-then-act on a path) +- wrong permissions, bad input, denial of service (panic / OOM still “stops,” still a bug) +- wrong FFI contracts +- bugs **inside** `unsafe` +- bugs in rustc or LLVM + +## Safety is not correctness + +```text +Memory safe + | + v +No use-after-free +No dangling pointer +No out-of-bounds write in safe code + | + v +BUT the program can still + | + v +wrong algorithm +wrong file / permissions +wrong protocol +TOCTOU +ignored Result +DoS (panic loop, OOM) +``` + +uutils is that picture in production: overflow and UAF got much harder. File races, `.ok()`, and C wrappers remained. Memory safety is a floor. It is not the building. + I used to think “the borrow checker is the whole story.” Then I searched a real crate for `unsafe`. The hole can be in your `unsafe` block, in a library, in C, or in rustc. The layer above can still look fine. ```mermaid @@ -836,25 +974,73 @@ Is a project with 500 `unsafe` blocks still safer than C? There is no yes/no. Tw Calling C is the same idea with a C API instead of a crate name. `from_raw_parts(pointer, length)`: the length came from C. rustc never checked it. -## A real project: uutils +## A real CVE, two ways + +**uutils** is GNU coreutils rewritten in Rust (`ls`, `dd`, `cp`, …). Ubuntu 25.10 ships it. Canonical paid Zellic to review it. I want one pair of bugs in the form security people actually use: what happened, why the language allowed it, would **safe** Rust have stopped it, what still goes wrong. + +### CVE-2026-56392 (GNU `unexpand`): heap overflow -**uutils** is GNU coreutils rewritten in Rust (`ls`, `dd`, `cp`, …). Ubuntu 25.10 ships it. Canonical paid a security firm (Zellic) to review it. +**What happened.** Tab-stop count times element size wrapped. The allocator got a small buffer. The write loop used the old count. Heap overflow. [OSV](https://osv.dev/vulnerability/CVE-2026-56392). CERT Polska also lists [CVE-2026-56391](https://cert.pl/en/posts/2026/07/CVE-2026-56391/) (out-of-bounds read). -The public write-up [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) says: the CVEs were mostly file races, permission bugs, “not the same as GNU,” and ignored errors. One example: [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc): `dd` hid a truncate error with `.ok()`. They did **not** report classic overflow / UAF. GNU, in a similar time window, still had heap overwrites. +**Why C allowed it.** Integer wrap plus `malloc` plus a write past the allocation is undefined behavior. The type system does not track “this length is the size I allocated.” -I searched a 2026 tree. About two hundred `unsafe` hits in `src/`. One bad case: a BSD C function can return length 0 and a null pointer. The Rust wrapper only rejected negative length, then built a slice from null. That is undefined behavior from a wrong C check. +**Would safe Rust prevent it?** The overflow-as-smash, usually yes: `Vec` length is the allocation; `v[i]` panics or you use `checked` / saturating math on purpose. A logic error that picks the **wrong** tab list is still possible. -I expected the “interesting” bugs to go away. They did not. Overflow and UAF got much harder. The remaining bugs moved to files, C, ignored `Result`s, and later the compiler. That is not “Rust has no CVEs.” It is also not “the rewrite was useless.” +**What could still go wrong in Rust?** Panic as DoS. Wrong output. An `unsafe` wrapper around a C allocator with a length you computed yourself: you are back in C. + +### CVE-2026-35344 (uutils `dd`): ignored error + +**What happened.** `dd` hid a truncate failure with `.ok()`. [Advisory](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc). The process looked successful. Data was not what the user asked for. + +**Why “Rust” allowed it.** This is not a memory bug. `Result` is a value. `.ok()` throws the `Err` away. rustc does not know you needed that truncate. + +**Would safe Rust prevent it?** No. Memory stayed fine. The **program** was wrong. + +**What could still go wrong?** Same class as every `unwrap` / `.ok()` / ignored `io::Error`. Level 1 does not care. + +### CVE-2026-35359 (uutils `cp`): TOCTOU + +**What happened.** `cp` checked a path, then opened it without `O_NOFOLLOW`. Swap a symlink in the gap. [oss-security](https://www.openwall.com/lists/oss-security/2026/05/02/2). Ubuntu 26.04 still ships GNU `cp` / `mv` / `rm` for that reason. + +**Would safe Rust prevent it?** No. See [the TOCTOU test](#4-file-race-toctou). rustc compiled it. ASan was silent. + +Zellic’s public write-up through [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) and Canonical’s [update](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773): mostly file races, permissions, “not the same as GNU,” ignored errors. They did **not** report classic overflow / UAF as the main pile. That is the rewrite working **and** the guarantee being smaller than the slide. + +I searched a 2026 tree. About two hundred `unsafe` hits in `src/`. One bad case: a BSD C function can return length 0 and a null pointer. The Rust wrapper only rejected negative length, then built a slice from null. That is Level 3: wrong C check, then `from_raw_parts`. ## The compiler can also be wrong -Safe Rust is only as strong as rustc. Code goes: types → rustc checks → MIR (Rust’s middle IR) → LLVM → machine code. Any step can fail. +Safe Rust is only as strong as rustc. If rustc **accepts an invalid program**, the Level 1 guarantee does not hold for that program. The language spec said no. The implementation said yes. LLVM then optimizes as if the type were true. + +```text +Rust source + | + v +Type checking + | + v +Borrow checking + | + v +MIR (Rust's middle IR) + | + v +Optimization (includes noalias from &mut) + | + v +LLVM + | + v +Machine code +``` + +Any arrow can lie. I used to ignore LLVM `noalias`. Then I saw what happens if rustc **wrongly** says a pointer lives forever (`&'static`). LLVM may treat that pointer as real and delete loads it thinks are impossible. **Memory safety** (don’t smash the heap) is not the same as **memory-model rules** (what the optimizer is allowed to assume). `&mut` means “only I can write.” rustc turns that into `noalias` for LLVM. If those two stories disagree, even “safe” code can be compiled wrong. -I used to ignore LLVM `noalias`. Then I saw what happens if rustc **wrongly** says a pointer lives forever (`&'static`). LLVM may treat that pointer as real and delete loads it thinks are impossible. **Memory safety** (don’t smash the heap) is not the same as **memory-model rules** (what the optimizer is allowed to assume). `&mut` means “only I can write.” rustc turns that into `noalias` for LLVM. If those two stories disagree, even “safe” code can be compiled wrong. +A pointer is not “just a number.” A hole in lifetime rules is not a word game. It is permission for the optimizer. -A pointer is not “just a number.” Most internet fights skip that. A hole in lifetime rules is not a word game. It is permission for the optimizer. +The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws the C++ side next to this. -The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws those steps. +Question for compiler people, not Twitter: **if rustc incorrectly accepts invalid code, does the safety guarantee still hold?** No. That is why ISSTA 2026 and #25860 sit in this article. They are not a gotcha against `Vec`. They are a limit on Level 1. ## A 2026 research paper @@ -918,19 +1104,23 @@ I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, all Normal app code does not look like this. If you start a tools argument with this file, people will say “that is a compiler bug.” They are right. Start with docs search if that is your point. I keep this file because I ran it. -## Old tool complaints, today - -Around 2015, some Rust users said: skip the borrow-checker fight, look at the tools. Some of that is fixed: serde, `impl Trait` (since 1.26), `cargo install` and cargo-dist, `const N: usize`. Three things are not. +## What you pay for the checks -**Docs search.** I typed `replace` in rustdoc on the `String` page. The methods from `str` are listed if you scroll. Search still does not find them through `Deref`. rust-analyzer (the editor helper) does. The website is still weak for “I don’t know the name yet.” +The guarantee is not free. This is the bill I actually hit. -**Compile time.** rustc is still slow. A parallel frontend is a 2026 goal (about 20–30% faster in tests, not the default yet). Small extra wins exist. People have said “the future looks good” for a long time. +- **Learning:** ownership and lifetimes. The first month is slower than C++ if you already know C++. +- **Compile time.** rustc is still slow. A parallel frontend is a 2026 goal (about 20–30% faster in tests, not the default yet). Waiting is a real cost on large crates. +- **Docs.** I typed `replace` in rustdoc on the `String` page. Methods from `str` are listed if you scroll. Search still does not find them through `Deref`. rust-analyzer does. Weak for “I don’t know the name yet.” +- **Runtime index checks** on `a[i]` when rustc cannot prove `i`. Usually cheap. Hot loops sometimes use `get_unchecked` (`unsafe`, Level 2). +- **`unsafe` boundaries and FFI.** You still write C ABI glue. That glue is where Level 1 ends. +- **Layout control.** Packed structs, custom allocators, MMIO: you will touch `unsafe` or stay in C. +- **No std lending iterator.** A standard `Iterator` cannot yield a borrow from inside itself. Other crates exist ([rust-streaming](https://github.com/emk/rust-streaming)). -**Streaming iterators.** A standard `Iterator` cannot yield a borrow from inside itself. You still cannot write, in std, a parser that hands out `&str` from its own buffer. Other crates exist ([rust-streaming](https://github.com/emk/rust-streaming)). +Around 2015 some people said: skip the borrow-checker fight, look at the tools. serde, `impl Trait`, `cargo install` got better. The three bullets above (docs search, compile wait, lending iterator) did not vanish. -A May 2023 [forum thread](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) asked why people dislike Rust. Some said inline assembly is awkward, or “Linux only has apt-get, they did not add Rust.” Fair replies: a tiny part of a kernel is special CPU instructions; Rust and C can live together; nobody will rewrite a billion lines of old C. In 2026, some Linux kernel code is Rust, most is still C. Special CPU ops still live in `.S` assembly files, like in C kernels. A lot of online hate is hype-backlash, not “`Vec` has no bounds check.” +A May 2023 [forum thread](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) asked why people dislike Rust. Fair replies: a tiny part of a kernel is special CPU instructions; Rust and C can live together; nobody will rewrite a billion lines of old C. In 2026 some Linux kernel code is Rust, most is still C. -Docs search and compile wait are one argument. #25860 is another. I mix them when I talk. They are not the same. +Docs search and compile wait are one argument. #25860 is another. Do not mix them. ## Would I pick Rust? @@ -938,11 +1128,23 @@ Yes, for new code where ownership is hard: a parser, a cache with threads, a sma No, as a “moral upgrade” of a huge old C/C++ SDK wrapper, or a math kernel that is already correct and fast in C++. Waiting on rustc is a real cost on those teams. +These stars are **taste, not a score**. I would not defend them in a standards meeting. They are how I explain the trade to a teammate in five minutes. + +| Situation | C | C++ | Rust | +|---|---:|---:|---:| +| Existing legacy code | ★★★★★ | ★★★★★ | ★★ | +| New systems component | ★★★★ | ★★★★ | ★★★★★ | +| Memory safety as the main risk | ★★ | ★★★ | ★★★★★ | +| Maximum ecosystem / ABI compatibility | ★★★★★ | ★★★★★ | ★★★ | +| Kernel / embedded | ★★★★★ | ★★★★ | ★★★★ | +| Rewrite a large C/C++ tree | ★★★★ | ★★★★★ | ★★ | +| New security-sensitive component | ★★★ | ★★★ | ★★★★★ | + “Rewrite it in Rust” is usually a bad plan. New drivers or a new sealed component can be a plan. For one new cache, see the [Rust vs C++ comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype). C++ already copied some ideas: RAII, smart pointers, `span`, sanitizers. `string_view` also made dangling pointers easier to type. The real question is the **default**. Sanitizers are extra flags and miss paths you never run. rustc checks safe code by default. rustc still has holes. -When someone says “Rust solved memory safety,” I now ask: safe code or kernel wrapper? which rustc? which kind of bug? how much `unsafe` is in the tree? When someone says “Rust is hype,” I ask: did they show a use-after-free with no `unsafe` and not a known compiler bug? The `transmute` snippet is not that demo. That is the escape hatch. I compiled that too. +When someone says “Rust solved memory safety,” I now ask: Level 1, 2, or 3? which rustc? which kind of bug? When someone says “Rust is hype,” I ask: did they show a use-after-free with no `unsafe` and not a known compiler bug? The `transmute` snippet is not that demo. That is the escape hatch. I compiled that too. ## Limits From 9d54b6f0c1458cbbc85d5bc1d79025f9357c062d Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:00:59 +0530 Subject: [PATCH 07/14] Connect Rust's safety claim to the borrow checker, LLVM, and data races. Adds what rustc actually proves, how that information reaches LLVM, panic vs undefined behavior, a compiled two-thread example, and a sharper closing sentence. --- docs/articles/rust-claims-a-reality-check.md | 278 +++++++++++++++++-- 1 file changed, 249 insertions(+), 29 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index c77c22be..069532f3 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -72,6 +72,28 @@ This is not “Rust is fake.” It is also not “Rust already fixed everything. What follows is the **top of the iceberg**: a few programs, a sanitizer flag, one rewrite (uutils), one rustc paper. Under that sit file races, C FFI, `unsafe` in libraries, optimizer rules, and bugs **inside rustc**. Those do not show up on the slide. They still ship. +The rest of the page answers one question: **what does rustc actually prove, how does that reach LLVM, and where does the proof stop?** + +```text + RUST SAFETY + | + +----------------+----------------+ + | | | + LANGUAGE TOOLCHAIN REAL WORLD + | | | + ownership rustc bugs FFI + borrowing LLVM bugs TOCTOU + lifetimes codegen OS APIs + bounds optimizer unsafe crates + data races logic bugs + | | | + +----------------+----------------+ + | + v + WHAT IS ACTUALLY + GUARANTEED? +``` + There are three different fights online. Do not mix them. 1. **Memory safety**: can the program smash memory? @@ -84,20 +106,24 @@ Those are different questions. - [The short answer](#the-short-answer) - [Three levels of Rust safety](#three-levels-of-rust-safety) +- [What the borrow checker actually proves](#what-the-borrow-checker-actually-proves) - [What “memory safe” means here](#what-memory-safe-means-here) - [The same bug in C, C++, and Rust](#the-same-bug-in-c-c-and-rust) - [What Rust guarantees](#what-rust-guarantees) +- [Memory safety is not undefined behavior](#memory-safety-is-not-undefined-behavior) +- [Data races](#data-races) - [Safety is not correctness](#safety-is-not-correctness) - [What I compiled](#what-i-compiled) - [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates) -- [The `unsafe` keyword](#the-unsafe-keyword) +- [The real difference is the default](#the-real-difference-is-the-default) +- [`unsafe` is a proof boundary](#unsafe-is-a-proof-boundary) - [Libraries and C code](#libraries-and-c-code) - [A real CVE, two ways](#a-real-cve-two-ways) -- [The compiler can also be wrong](#the-compiler-can-also-be-wrong) +- [From borrow checking to LLVM](#from-borrow-checking-to-llvm) - [A 2026 research paper](#a-2026-research-paper) - [Bug #25860, which I compiled](#bug-25860-which-i-compiled) - [What you pay for the checks](#what-you-pay-for-the-checks) -- [Would I pick Rust?](#would-i-pick-rust) +- [The claim I would actually make](#the-claim-i-would-actually-make) - [Limits](#limits) - [References](#references) @@ -122,7 +148,7 @@ What each side actually offers: :::tip **The catch is the default.** In `C` and `C++`, writing beyond the length of an array can still compile, but it may cause a `memory bug at runtime`. -In `Rust`, the `compiler` usually catches this during `compilation time` ,hence the program can be `safe during runtme`. +In `Rust`, the `compiler` usually catches this during compilation, so the program does not smash memory at runtime. ::: A stricter compiler and new types do not delete human error. They move it. Split who is typing: @@ -348,6 +374,80 @@ The slide says “Rust.” That is three different products glued together. The rest of this article is that picture filled in with programs. +Think of a running binary as trust boundaries. The guarantee is strongest at the top. + +```text + APPLICATION + | + v + +-------------+ + | Safe Rust | + +-------------+ + | + borrow / type checks + | + v + +-------------+ + | unsafe | + +-------------+ + | + programmer proof + | + v + +-------------+ + | FFI | + +-------------+ + | + C / ABI / OS + | + v + +-------------+ + | LLVM | + +-------------+ + | + v + Hardware +``` + +## What the borrow checker actually proves + +People say the borrow checker “prevents memory bugs.” True, and incomplete. It does **not** watch machine code. It checks ownership, lifetimes, and aliases **before** LLVM. + +```rust +fn use_string() { + let s = String::from("hello"); + let r = &s; + println!("{}", r); +} +``` + +`s` owns the heap `String`. `r` only borrows. rustc’s rule is: **owner lives at least as long as the borrow.** + +Move the owner first: + +```rust +fn example() { + let s = String::from("hello"); + let r = &s; + drop(s); + println!("{}", r); +} +``` + +```text +s owns memory + | + +---- r borrows it + | + +---- s is destroyed + | + X r is still alive +``` + +I compiled that shape as [Example 1](#the-short-answer) (`&s` returned from the function). rustc: `E0515`. No binary. + +ASan asks: “did this **run** access dead memory?” The borrow checker asks: “can this program even **name** that relationship?” Sanitizer: observe an execution. Borrow checker: reject the program. + ## What “memory safe” means here **Memory safety** means: the program only reads and writes memory it is allowed to use, and only while that memory is still alive. Two threads should not write the same memory at the same time with no lock. @@ -463,6 +563,46 @@ Rust’s memory-safety claim applies to **safe Rust**, if rustc is sound, and if - bugs **inside** `unsafe` - bugs in rustc or LLVM +## Memory safety is not undefined behavior + +These get mixed. They are cousins, not twins. + +Safe Rust `v[100]` on a length-3 `Vec` is **not** an undefined access. rustc emits a bounds check. Index does not fit: **panic**. The language still has a defined meaning: stop. + +C `v[100]` on `int v[3]`: the language does **not** define that as a normal out-of-range read. The compiler may assume valid programs never do it. That is extra freedom for the optimizer, including deleting branches a human thought were live. “Undefined” is not “it crashes.” It is “the rest of the program no longer has the usual rules.” + +```text +Safe Rust: invalid index -> panic (defined) +Unsafe Rust: broken contract -> undefined behavior +C / C++: many bugs are UB from the first bad access +``` + +`unsafe` is that second path. Not “weird syntax.” A hole in the proof. + +## Data races + +Memory safety is also threads. I compiled this: + +```rust +fn main() { + let mut n = 0i32; + std::thread::scope(|s| { + s.spawn(|| { n += 1; }); + s.spawn(|| { n += 1; }); + }); +} +``` + +```text +error[E0499]: cannot borrow `n` as mutable more than once at a time +``` + +No binary. Two closures both want `&mut n`. rustc refuses. + +The C++ cousin `int counter = 0; void worker() { counter++; }` compiled with g++ and clang++ 18, `-Wall -Wextra`, exit 0. Two threads on that `worker` is a data race. `counter++` is not one machine step: load, add, store. Both threads can store the same old value. + +Safe Rust wants `Arc>` or an `AtomicUsize`. That is not “concurrent programs are correct.” You can still deadlock, starve, lock in the wrong order, or race on **files**. The type system only blocks **unsynchronized conflicting memory access** in safe code. + ## Safety is not correctness ```text @@ -952,10 +1092,34 @@ Rust does **not** need a Boost-the-project. [`std`](https://doc.rust-lang.org/st None of that makes `span[i]` check bounds by default. Libraries do not replace the compiler’s default. -## The `unsafe` keyword +## The real difference is the default + +C++ is not empty of safety tools. RAII, `unique_ptr`, `span`, `vector::at`, `optional`, atomics, sanitizers. The hole is there is **no one mandatory model**. `v[i]` is unchecked. `v.at(i)` checks. I compiled both. + +Rust’s normal `v[i]` is the checked one. The unchecked spelling is a different API and sits in `unsafe`: + +```rust +unsafe { *v.get_unchecked(i) } +``` + +The useful question is not “does C++ have a safe API?” It does. It is: **what happens if the programmer forgets?** That is the default. + +## `unsafe` is a proof boundary `unsafe` means: “compiler, trust me here.” It is not a confession that Rust failed. It is the door out of the proof. +A common line: “Rust has `unsafe`, so it is C.” Too simple. The point is the hole is **named**, and you can keep it small. + +```rust +pub fn first_byte(data: &[u8]) -> u8 { + unsafe { *data.as_ptr() } +} +``` + +Callers stay in safe Rust. Inside, you own the invariant (`data` not empty). Ten lines of `unsafe` under a thousand lines of safe code is the design. Five thousand lines of `transmute`, raw pointers, and FFI is C with extra steps. + +Counting `unsafe` blocks is not a science score. The question is: **what invariant does this block establish, and who relies on it?** + This is the first trick people send: ```rust @@ -966,8 +1130,6 @@ pub fn as_static(s: &str) -> &'static str { `main` has no `unsafe`. The program can still use memory after it is freed. Why? rustc checks the **function type**, not the proof inside `unsafe`. `std` uses `unsafe` too, on purpose: hide the dangerous bit. The bad case is when that hiding is a lie. -Is a project with 500 `unsafe` blocks still safer than C? There is no yes/no. Two small `unsafe` blocks behind a clean API is the design. A crate that is basically C with Rust syntax is C with extra steps. I count, roughly: `unsafe` blocks, `unsafe fn`, `extern`, raw pointer tricks, and things rustc cannot see (file descriptors, `mmap`). That is not a science score. It is a way to talk in numbers. - ## Libraries and C code “Our crate has no `unsafe`” is not the full story. Your `Cargo.lock` may pull in other crates that do. `cargo audit` finds **known** security reports. It does not prove every library is correct. @@ -1008,39 +1170,80 @@ Zellic’s public write-up through [Bugs Rust Won’t Catch](https://corrode.dev I searched a 2026 tree. About two hundred `unsafe` hits in `src/`. One bad case: a BSD C function can return length 0 and a null pointer. The Rust wrapper only rejected negative length, then built a slice from null. That is Level 3: wrong C check, then `from_raw_parts`. -## The compiler can also be wrong +## From borrow checking to LLVM Safe Rust is only as strong as rustc. If rustc **accepts an invalid program**, the Level 1 guarantee does not hold for that program. The language spec said no. The implementation said yes. LLVM then optimizes as if the type were true. +The borrow checker is not the last pass. + ```text Rust source - | - v + | + v +AST + | + v +HIR + | + v Type checking - | - v + | + v Borrow checking - | - v -MIR (Rust's middle IR) - | - v -Optimization (includes noalias from &mut) - | - v -LLVM - | - v + | + v +MIR + | + v +MIR optimization + | + v +LLVM IR + | + v Machine code ``` -Any arrow can lie. I used to ignore LLVM `noalias`. Then I saw what happens if rustc **wrongly** says a pointer lives forever (`&'static`). LLVM may treat that pointer as real and delete loads it thinks are impossible. **Memory safety** (don’t smash the heap) is not the same as **memory-model rules** (what the optimizer is allowed to assume). `&mut` means “only I can write.” rustc turns that into `noalias` for LLVM. If those two stories disagree, even “safe” code can be compiled wrong. +Ownership is checked **before** LLVM. Some of it still shows up in IR. `&mut i32` is not “any C pointer.” For that lifetime, no other conflicting write. rustc can lower that as LLVM `noalias`. The optimizer may delete loads it thinks are impossible. + +If the frontend was **wrong**, the optimizer is still “correct” relative to the IR it was given. That is a miscompile of the *language*, not a random backend crash. + +```text +language rules + | + v +borrow checking + | + v +unsafe contracts + | + v +compiler correctness + | + v +optimizer correctness +``` + +The guarantee is not “the borrow checker is perfect.” It is: **the whole pipeline preserves the language’s safety rules.** -A pointer is not “just a number.” A hole in lifetime rules is not a word game. It is permission for the optimizer. +| Layer | What can go wrong | +|---|---| +| Parser | Accepts / rejects the wrong syntax | +| Type checker | Wrong type judgment | +| Borrow checker | Accepts a bad lifetime or alias | +| MIR | Bad transform | +| MIR optimizer | Miscompiles valid Rust | +| LLVM lowering | Wrong IR | +| LLVM optimizer | Wrong transform (`noalias` on a lie) | +| Backend / linker | Wrong instruction or layout | + +I used to ignore LLVM `noalias`. Then I saw what happens if rustc **wrongly** says a pointer lives forever (`&'static`). LLVM may treat that pointer as real. **Memory safety** (don’t smash the heap) is not the same as **memory-model rules** (what the optimizer may assume). `&mut` means “only I can write.” If those two stories disagree, even “safe” code can be compiled wrong. + +A pointer is not “just a number.” A hole in lifetime rules is permission for the optimizer. The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws the C++ side next to this. -Question for compiler people, not Twitter: **if rustc incorrectly accepts invalid code, does the safety guarantee still hold?** No. That is why ISSTA 2026 and #25860 sit in this article. They are not a gotcha against `Vec`. They are a limit on Level 1. +Question for compiler people: **if rustc incorrectly accepts invalid code, does the safety guarantee still hold?** No. That is why ISSTA 2026 and #25860 sit here. They are not a gotcha against `Vec`. They are a limit on Level 1. ## A 2026 research paper @@ -1122,7 +1325,7 @@ A May 2023 [forum thread](https://users.rust-lang.org/t/why-are-some-people-agai Docs search and compile wait are one argument. #25860 is another. Do not mix them. -## Would I pick Rust? +## The claim I would actually make Yes, for new code where ownership is hard: a parser, a cache with threads, a small C API you can wrap. @@ -1142,9 +1345,26 @@ These stars are **taste, not a score**. I would not defend them in a standards m “Rewrite it in Rust” is usually a bad plan. New drivers or a new sealed component can be a plan. For one new cache, see the [Rust vs C++ comparison](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype). -C++ already copied some ideas: RAII, smart pointers, `span`, sanitizers. `string_view` also made dangling pointers easier to type. The real question is the **default**. Sanitizers are extra flags and miss paths you never run. rustc checks safe code by default. rustc still has holes. +I would not write: “Rust makes programs memory safe.” Too broad. I would write: + +**Safe Rust moves a large class of memory-safety errors from runtime into compile-time rules. `unsafe`, FFI, compiler bugs, OS interfaces, and logic errors stay outside that guarantee.** + +Less marketable. More useful. + +The engineering question is not “is Rust safe?” It is: **which invariants does rustc enforce for this code, and where does the programmer prove the rest?** + +```text +Safe Rust: ownership, borrows, lifetimes, bounds, data-race rules + -> compiler-enforced + +unsafe / FFI: raw pointers, C lengths, get_unchecked + -> you + +rustc -> MIR -> LLVM -> backend + -> compiler correctness +``` -When someone says “Rust solved memory safety,” I now ask: Level 1, 2, or 3? which rustc? which kind of bug? When someone says “Rust is hype,” I ask: did they show a use-after-free with no `unsafe` and not a known compiler bug? The `transmute` snippet is not that demo. That is the escape hatch. I compiled that too. +Rust did not delete the need for correctness. It moved a big piece of it into the type system. That is the achievement. ## Limits From 6d5bc2e697819709bc6d4755e5189b649975707f Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:15:04 +0530 Subject: [PATCH 08/14] Tighten the Rust claims article: one results table, less repetition. Cut overlapping recap sections, put compile-time vs panic vs unsafe up front, and keep TOCTOU as the boundary example instead of a second CVE essay. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 551 +++---------------- 1 file changed, 74 insertions(+), 477 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index 069532f3..c4135c66 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -1,6 +1,6 @@ --- title: "Rust Claims, a Reality Check: Safety, Tools, and Systems Programming" -description: "A plain-English look at what 'Rust is memory safe' really means: what the compiler stops, what it does not, and a real compiler bug." +description: "What rustc actually proves, how that reaches LLVM, and where the proof stops. Compiled on rustc 1.93.1 vs gcc 13.3 and clang 18." keywords: - Rust memory safety - Rust safety guarantees @@ -60,19 +60,17 @@ import Head from '@docusaurus/Head'; -# Rust Claims, a Reality Check: Safety, Tools, and Systems Programming +# Rust Claims, a Reality Check: What rustc Proves, and Where the Proof Stops :::note Related: [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [How rustc compiles vs C++](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) ::: -People say **Rust is memory safe**. I wanted to know what that actually means. So I compiled small programs, looked at a real Rust project, and read a 2026 paper about bugs in rustc (the Rust compiler). +People say **Rust is memory safe**. I wanted the compiler version of that sentence, not the slide. So I compiled the same bugs with rustc 1.93.1, gcc/g++ 13.3, and clang 18, then asked where the proof stops: language, rustc, LLVM, or the OS. -This is not “Rust is fake.” It is also not “Rust already fixed everything.” It is: the claim is real, but it is smaller than the short sentence on slides. +C++ already has `unique_ptr`, `span`, `v.at(i)`, sanitizers. The interesting difference is **what happens when the programmer forgets to use them.** Rust’s normal `v[i]` is checked. C++’s normal `v[i]` is not. -What follows is the **top of the iceberg**: a few programs, a sanitizer flag, one rewrite (uutils), one rustc paper. Under that sit file races, C FFI, `unsafe` in libraries, optimizer rules, and bugs **inside rustc**. Those do not show up on the slide. They still ship. - -The rest of the page answers one question: **what does rustc actually prove, how does that reach LLVM, and where does the proof stop?** +The claim is real. It is also smaller than “Rust is memory safe.” ```text RUST SAFETY @@ -80,45 +78,40 @@ The rest of the page answers one question: **what does rustc actually prove, how +----------------+----------------+ | | | LANGUAGE TOOLCHAIN REAL WORLD - | | | - ownership rustc bugs FFI - borrowing LLVM bugs TOCTOU - lifetimes codegen OS APIs - bounds optimizer unsafe crates - data races logic bugs - | | | - +----------------+----------------+ - | - v - WHAT IS ACTUALLY - GUARANTEED? + ownership rustc FFI + borrowing LLVM TOCTOU + bounds optimizer logic / .ok() + data races OS APIs ``` -There are three different fights online. Do not mix them. +**Results of the programs I compiled** (default flags unless noted: `-Wall -Wextra`, no sanitizer): -1. **Memory safety**: can the program smash memory? -2. **Tools**: is rustc slow? is the docs search bad? -3. **Systems work**: can you write a kernel, or only small apps? +| Bug | C/C++ default | C/C++ + ASan/UBSan | Safe Rust | +|---|---|---|---| +| Use-after-free | Compiles (gcc may warn; clang often silent) | Runtime abort | Rejected (`E0515` / `E0382`) | +| Constant OOB (`a[10]` on size 4) | Compiles (clang warns, still links) | Runtime report | Rejected (compile error) | +| Runtime OOB (`a[i]`) | UB; my run smashed a neighbor `flag` | Often a message; default UBSan still continues | Panic, exit 101 | +| Data race (`n += 1` from two threads) | Compiles | Tool-dependent | Rejected (`E0499`) | +| TOCTOU (check path, swap, open) | Compiles; reads the swapped file | ASan silent | Compiles; same wrong file | +| Bad FFI length / `unsafe` lie | Possible | Not solved | Possible (Level 2–3) | +| Logic / ignored `Result` | Possible | Not solved | Possible | + +Evidence is below. Three different index stories, say them once: -Those are different questions. +1. **Constant index rustc can see** → compile-time rejection. No binary. +2. **Runtime index** → rustc emits a bounds check → **panic** (defined). Not C undefined behavior. +3. **`unsafe { *v.get_unchecked(i) }`** → you hold the proof. Broken invariant is UB. ## Table of Contents - [The short answer](#the-short-answer) - [Three levels of Rust safety](#three-levels-of-rust-safety) - [What the borrow checker actually proves](#what-the-borrow-checker-actually-proves) -- [What “memory safe” means here](#what-memory-safe-means-here) - [The same bug in C, C++, and Rust](#the-same-bug-in-c-c-and-rust) -- [What Rust guarantees](#what-rust-guarantees) -- [Memory safety is not undefined behavior](#memory-safety-is-not-undefined-behavior) - [Data races](#data-races) -- [Safety is not correctness](#safety-is-not-correctness) - [What I compiled](#what-i-compiled) -- [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates) -- [The real difference is the default](#the-real-difference-is-the-default) +- [C++23 and sanitizers](#c23-and-sanitizers) - [`unsafe` is a proof boundary](#unsafe-is-a-proof-boundary) -- [Libraries and C code](#libraries-and-c-code) -- [A real CVE, two ways](#a-real-cve-two-ways) - [From borrow checking to LLVM](#from-borrow-checking-to-llvm) - [A 2026 research paper](#a-2026-research-paper) - [Bug #25860, which I compiled](#bug-25860-which-i-compiled) @@ -129,50 +122,7 @@ Those are different questions. ## The short answer -**Safe Rust** (code with no `unsafe` keyword) really does stop many memory bugs that C and C++ still allow. C and C++ can catch some of the same bugs with [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer) / [UBSan](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html). Those flags are extra. rustc’s check on the examples below is the default. - -The argument I hear is real: - -- C and C++ still have memory bugs -- so they need a **stricter compiler**, or **language features** that make those bugs harder to type - -What each side actually offers: - -- **Rust:** both. Language rules cover two things rustc checks by **default**: - - **borrow checking**: who owns this memory, and whether a pointer to it is still valid - - **index checking**: the array has 4 slots; writing slot 10 is an error -- **C++:** - - language features you can pick: [`std::unique_ptr`](https://en.cppreference.com/w/cpp/memory/unique_ptr), [`std::span`](https://en.cppreference.com/w/cpp/container/span), [`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at). - - Plus a stricter compiler you can turn on: [ASan](https://github.com/google/sanitizers/wiki/AddressSanitizer) / [UBSan](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html). - -:::tip **The catch is the default.** -In `C` and `C++`, writing beyond the length of an array can still compile, but it may cause a `memory bug at runtime`. - -In `Rust`, the `compiler` usually catches this during compilation, so the program does not smash memory at runtime. -::: - -A stricter compiler and new types do not delete human error. They move it. Split who is typing: - -- **C/C++ app developer**: the safer types exist, but they do not have to use them. They can still: - - index with `v[i]` instead of `v.at(i)` (no check) - - use `new` / `delete` instead of `unique_ptr` (easy to free too soon) - - `free(p)` then `printf("%s", p)` (use memory after it is gone) - - write `a[10]` on an array of size 4 (past the end) -- **Rust app developer**: can write [`unsafe`](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html), lie to the type system, or pass a bad length into C -- **gcc / g++ / clang developer**: can miss a warning, ship bad codegen, or an optimizer bug -- **rustc developer**: can ship a [soundness bug](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) (accept code they should reject) - -Same species. Features only help if the app developer uses the safe default (Rust) or the safe API (C++ `v.at(i)`, not `v[i]`). - -The useful question is not who is smarter. It is **what each language gives you to find the mess**: - -- default rustc: language rules on -- default gcc / g++ / clang: those rules off -- C++ features you choose: `at()`, `span`, smart pointers -- sanitizers: if you pass the flag and run that path -- [Miri](https://github.com/rust-lang/miri): after rustc already said ok, for rustc holes - -Let’s look at examples I compiled with rustc 1.93.1, gcc/g++ 13.3, and **clang/clang++ 18.1.3**. Full output is in [What I compiled](#what-i-compiled) and [C++23, sanitizers, Boost, crates](#c23-sanitizers-boost-crates). +The table above is the claim. These three programs are the evidence. Toolchains: rustc 1.93.1, gcc/g++ 13.3, clang/clang++ 18.1.3. Full logs: [What I compiled](#what-i-compiled). **Example 1.** This function makes a `String` on the stack, then tries to hand a pointer to it back to the caller: @@ -326,88 +276,33 @@ still running a[0]=0 flag=42 UBSan **printed** the same “index out of bounds” story Rust panics with. Then the program **kept running** and `flag` was still smashed. Default UBSan recovers. ASan did not stop this one: `a[4]` is the next field in the same `struct`, an intra-object overflow sanitizers often miss. -If I add `-fno-sanitize-recover=undefined`, UBSan aborts and does not print `still running`. That flag is extra, like ASan is extra. Rust’s panic on `a[i]` needed no extra flag. - -**What each side actually gives you** - -| Where the human erred | What C/C++ gave me | What Rust gave me | -|---|---|---| -| Safe-looking UAF / constant overflow | Binary, unless I add ASan/UBSan and run that path | rustc error, no binary | -| Runtime `a[i]` too big | Default: smash and continue. Sanitizer: maybe a message; maybe still continue; maybe miss | Panic, default, debug and `-O` | -| `unsafe` / FFI / “trust me” | Same as C: you are on your own | rustc trusts you. [Miri](https://github.com/rust-lang/miri) can check **if** you run it | -| rustc itself wrong | (n/a) | ISSTA 2026: rustc accepted code it should reject. Miri after the fact | - -So: sanitizers can report what Rust reports. They are a tool you turn on. Safe Rust is a default. That is the real difference, not “Rust programmers never err.” They do. `unsafe` is that err. The language still marks the hole (`unsafe`) and still panics in the safe subset. C does not mark `a[i] = 42` as unsafe, and the default `a[i]` does not panic. +If I add `-fno-sanitize-recover=undefined`, UBSan aborts and does not print `still running`. That flag is extra. Rust’s panic on `a[i]` needed no extra flag. -Rust does **not** magically stop: - -- logic bugs (the program does the wrong thing) -- file races (check a path, then someone changes the file) -- mistakes when talking to C -- bugs **inside rustc itself** - -A 2026 ISSTA paper, [Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) (Sim, Ryu, Hong; artifact on [Zenodo](https://doi.org/10.5281/zenodo.20698055)), counted rustc bugs where the compiler said “ok” to code it should have rejected. [Miri](https://github.com/rust-lang/miri) (a Rust checker) can catch some of those **after** rustc already accepted the code. That is Rust’s sanitizer-shaped answer for compiler holes: opt-in, after compile. - -Big companies use Rust because it moves one class of bugs to compile time. That is useful. The compiler is still software. Software has bugs. +Sanitizers can name the same bugs. They are a flag you pass and a path you must run. Safe Rust’s reject/panic is the default. C does not mark `a[i] = 42` as `unsafe`. rustc bugs and FFI still sit outside that default; those sections come later. ## Three levels of Rust safety -The slide says “Rust.” That is three different products glued together. +The slide says “Rust.” That is several products glued together. -**Level 1: safe Rust.** No `unsafe` in *your* function. rustc checks who owns the memory, how long a pointer is valid, and (for `a[i]`) whether the index fits. That is the claim people mean. +**Level 1: safe Rust.** No `unsafe` in *your* function. rustc checks ownership, lifetimes, aliases, and (for `a[i]`) whether the index fits. -**Level 2: `unsafe`.** You told rustc to trust you. The type still looks safe to callers. The proof is now a human. +**Level 2: `unsafe`.** You told rustc to trust you. Callers still see a safe type. The proof is a human. -**Level 3: FFI + the toolchain.** C libraries, ABI, file descriptors, `mmap`, rustc itself, LLVM. The language rules stop at the `extern "C"` door. They also stop if rustc is wrong. +**Level 3: FFI + the toolchain.** C libraries, ABI, file descriptors, `mmap`, rustc itself, LLVM. Language rules stop at `extern "C"`. They also stop if rustc is wrong. -```text - Rust safety - | - +----------+----------+ - | | | - Safe Rust unsafe FFI / toolchain - | | | - ownership you hold C, ABI, - borrowing the proof rustc, LLVM - index checks -``` +### Where the language guarantee ends -The rest of this article is that picture filled in with programs. +This is the compiler article’s spine. **Memory-safety as a language model** is not the same as **an implementation bug in rustc.** -Think of a running binary as trust boundaries. The guarantee is strongest at the top. +1. **Safe Rust** — ownership, borrows, bounds, data-race rules (if rustc is sound). +2. **`unsafe`** — you assume the invariant. +3. **FFI** — rustc trusts C lengths, pointers, and ABI. +4. **Library implementation** — `std` and crates contain `unsafe`; `Cargo.lock` is in the TCB. +5. **rustc** — a soundness bug accepts code the language forbids. That is not “Rust lied”; the *implementation* did. +6. **LLVM** — optimizes IR it was given (`noalias` on a lie is still “correct” LLVM). +7. **OS / hardware** — TOCTOU, `mmap`, permissions, cosmic rays. -```text - APPLICATION - | - v - +-------------+ - | Safe Rust | - +-------------+ - | - borrow / type checks - | - v - +-------------+ - | unsafe | - +-------------+ - | - programmer proof - | - v - +-------------+ - | FFI | - +-------------+ - | - C / ABI / OS - | - v - +-------------+ - | LLVM | - +-------------+ - | - v - Hardware -``` +The rest of the article fills that list with programs. ## What the borrow checker actually proves @@ -448,47 +343,7 @@ I compiled that shape as [Example 1](#the-short-answer) (`&s` returned from the ASan asks: “did this **run** access dead memory?” The borrow checker asks: “can this program even **name** that relationship?” Sanitizer: observe an execution. Borrow checker: reject the program. -## What “memory safe” means here - -**Memory safety** means: the program only reads and writes memory it is allowed to use, and only while that memory is still alive. Two threads should not write the same memory at the same time with no lock. - -It does **not** mean “the program is correct.” A program can be memory-safe and still delete the wrong file. - -Simple names: - -| Name | Plain meaning | -|---|---| -| Use-after-free (UAF) | Use memory after you freed it | -| Buffer overflow | Write past the end of an array | -| Double free | Free the same memory twice | -| Data race | Two threads touch the same memory in a bad way | -| Null | Use a pointer that is empty | -| Uninit | Read memory you never set | -| TOCTOU | Check a file, then it changes before you use it | -| FFI | Rust calling C (or C calling Rust) | -| Soundness bug | The compiler accepts code it should reject | - -| Kind of bug | Safe Rust | `unsafe` or C FFI | C / C++ | -|---|---|---|---| -| UAF, overflow, double-free, data race, null, uninit | Usually stopped | Possible | Possible | -| Integer wrap (numbers too big) | Debug: panic. Release: wrap. Not the same as C “undefined” smash | Same | Often dangerous | -| TOCTOU, logic bugs, out of memory | Not stopped | Not stopped | Not stopped | -| Bad C API / compiler bug | Not stopped | Possible | Possible | - -If you write `slice[i]` and `i` is too big, **safe Rust panics** (the program stops). That is the *safe* failure. In C the same index often corrupts memory and keeps running. [Example 3](#the-short-answer) is that test: Rust `./slice_i_rs 4` panics (exit 101). gcc `./slice_i_c 4` prints `still running` and `flag` changed from 7 to 42. - -One table for the experiments above (safe Rust, **default** C/C++ build, no sanitizer): - -| Bug / property | C | C++ | Safe Rust | -|---|---|---|---| -| Out-of-bounds **constant** index (`a[10]` on size 4) | Compiles (gcc silent; clang warns, still links) | Compiles | Compile error | -| Out-of-bounds **runtime** index | Undefined behavior; my run smashed a neighbor `flag` | Same with `span[i]` | Panic; process stops | -| Use-after-free | Possible; gcc may warn | Possible; `string_view` was silent | Ownership: no binary | -| Data race | Possible | Possible | Prevented in safe code | -| Null dereference | Possible | Possible | `Option` / references, not raw null | -| Manual `free` / `delete` | Yes | Yes | Usually unnecessary (`Box` / `Vec` drop it) | - -That is the takeaway. Sanitizers and `vector::at` move C++ closer to the right-hand column. They are not the default. +Safe `v[100]` on a length-3 `Vec` is **not** C undefined behavior. rustc emits a bounds check; miss → **panic** (defined stop). C `v[100]` on `int v[3]` is UB: the optimizer may assume it never happens. `unsafe` is the second Rust path: broken contract → UB. ## The same bug in C, C++, and Rust @@ -543,42 +398,6 @@ error[E0382]: use of moved value: `p` The longer `String` / `string_view` demos in [What I compiled](#what-i-compiled) are the same rule with more bytes. -## What Rust guarantees - -Rust’s memory-safety claim applies to **safe Rust**, if rustc is sound, and if you did not smuggle a lie through `unsafe` or C. It does **not** mean every Rust program is free of bugs. - -**Safe Rust is built to stop** - -- use-after-free, double-free, dangling references -- out-of-bounds access (compile error if rustc can see it; panic if `i` is a variable) -- data races (two threads, same memory, no lock, at least one write) -- using `null` as a value: you use `Option` instead - -**Safe Rust does not automatically stop** - -- logic bugs (wrong algorithm, wrong result) -- races that are **not** data races (TOCTOU, check-then-act on a path) -- wrong permissions, bad input, denial of service (panic / OOM still “stops,” still a bug) -- wrong FFI contracts -- bugs **inside** `unsafe` -- bugs in rustc or LLVM - -## Memory safety is not undefined behavior - -These get mixed. They are cousins, not twins. - -Safe Rust `v[100]` on a length-3 `Vec` is **not** an undefined access. rustc emits a bounds check. Index does not fit: **panic**. The language still has a defined meaning: stop. - -C `v[100]` on `int v[3]`: the language does **not** define that as a normal out-of-range read. The compiler may assume valid programs never do it. That is extra freedom for the optimizer, including deleting branches a human thought were live. “Undefined” is not “it crashes.” It is “the rest of the program no longer has the usual rules.” - -```text -Safe Rust: invalid index -> panic (defined) -Unsafe Rust: broken contract -> undefined behavior -C / C++: many bugs are UB from the first bad access -``` - -`unsafe` is that second path. Not “weird syntax.” A hole in the proof. - ## Data races Memory safety is also threads. I compiled this: @@ -601,43 +420,7 @@ No binary. Two closures both want `&mut n`. rustc refuses. The C++ cousin `int counter = 0; void worker() { counter++; }` compiled with g++ and clang++ 18, `-Wall -Wextra`, exit 0. Two threads on that `worker` is a data race. `counter++` is not one machine step: load, add, store. Both threads can store the same old value. -Safe Rust wants `Arc>` or an `AtomicUsize`. That is not “concurrent programs are correct.” You can still deadlock, starve, lock in the wrong order, or race on **files**. The type system only blocks **unsynchronized conflicting memory access** in safe code. - -## Safety is not correctness - -```text -Memory safe - | - v -No use-after-free -No dangling pointer -No out-of-bounds write in safe code - | - v -BUT the program can still - | - v -wrong algorithm -wrong file / permissions -wrong protocol -TOCTOU -ignored Result -DoS (panic loop, OOM) -``` - -uutils is that picture in production: overflow and UAF got much harder. File races, `.ok()`, and C wrappers remained. Memory safety is a floor. It is not the building. - -I used to think “the borrow checker is the whole story.” Then I searched a real crate for `unsafe`. The hole can be in your `unsafe` block, in a library, in C, or in rustc. The layer above can still look fine. - -```mermaid -flowchart TB - P[Your program] --> S[Safe Rust] - S --> BC[Compiler checks] - BC --> RC[Is rustc itself correct?] - RC --> DEP[Libraries + unsafe] - DEP --> FFI[C / OS] - FFI --> HW[Hardware] -``` +Safe Rust wants `Arc>` or an `AtomicUsize`. Deadlock, starvation, and **file** races are still possible. The type system only blocks **unsynchronized conflicting memory access** in safe code. ## What I compiled @@ -991,124 +774,46 @@ use: read "secret" -In the real world the swap is another process in the gap between check and use, not `rename` in the same `main`. Same hole. The fix is not a smarter rustc. It is: open **once**, then `fstat` / operate on the **file descriptor**, or `O_NOFOLLOW`, so the name is not looked up twice. +Rust can compile the program, ASan can stay silent, and the program can still use the wrong file. Memory safety is not general security. -Canonical paid [Zellic](https://github.com/Zellic/publications) to audit Ubuntu’s Rust coreutils ([uutils](https://github.com/uutils/coreutils)). Write-up: [An update on rust-coreutils](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773) (22 Apr 2026). Report: [uutils coreutils: Zellic Audit Report](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf). CVE list: [oss-security](https://www.openwall.com/lists/oss-security/2026/05/02/2). Example: [CVE-2026-35359](https://www.openwall.com/lists/oss-security/2026/05/02/2): `cp` checks a path, then opens it without `O_NOFOLLOW`; an attacker can swap in a symlink. Ubuntu 26.04 still ships GNU `cp` / `mv` / `rm` because those races were still open. +The production cousin is [CVE-2026-35359](https://www.openwall.com/lists/oss-security/2026/05/02/2) in Ubuntu’s Rust [uutils](https://github.com/uutils/coreutils) `cp`: check a path, open without `O_NOFOLLOW`, swap a symlink. Canonical / [Zellic](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf) found mostly file races and ignored errors, not a pile of UAF. GNU still shipped a heap overflow: [CVE-2026-56392](https://osv.dev/vulnerability/CVE-2026-56392). Different class, both real. uutils `dd` [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) hid a truncate failure with `.ok()`: memory fine, program wrong. -GNU coreutils still has **memory** bugs too. [CVE-2026-56392](https://osv.dev/vulnerability/CVE-2026-56392) (`unexpand`): integer wrap when sizing a buffer, then a heap overflow. [CERT Polska](https://cert.pl/en/posts/2026/07/CVE-2026-56391/) also lists [CVE-2026-56391](https://cert.pl/en/posts/2026/07/CVE-2026-56391/) (out-of-bounds read). So: uutils got TOCTOU CVEs; GNU still got overflow CVEs. Different class, both real. My test above is the TOCTOU class: rustc green, ASan green, wrong file. +Fix for the experiment: open **once**, then `fstat` / operate on the **file descriptor**, or `O_NOFOLLOW`. ### 5. Talking to C -When Rust calls C (`extern "C"`), rustc trusts the C side. That is [FFI](https://doc.rust-lang.org/nomicon/ffi.html) (foreign function interface). The language rules: [external blocks](https://doc.rust-lang.org/reference/items/external-blocks.html). Wrong length. A null pointer that the C docs call “success.” A memory map that another process shrinks. That is not the borrow checker failing. That is a contract with C. If you then do `from_raw_parts(ptr, len)`, rustc never measured `len`. - -If a talk only shows tests 1 and 2, they showed the *compile-time* claim. Test 3 is the *run-time* panic. Tests 4 and 5 are the rest of the story. - -People also point at “out of memory, process dies” or “program panics” and say Rust is not safe. Read the [panic docs](https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html). A panic unwinds or aborts. Allocation failure calls [`handle_alloc_error`](https://doc.rust-lang.org/std/alloc/fn.handle_alloc_error.html) (usually abort). Those are still bugs. They are not [`strcpy`](https://en.cppreference.com/w/c/string/byte/strcpy) past a buffer. `strcpy` is [undefined behavior](https://en.cppreference.com/w/c/language/behavior): the program may keep running on smashed memory. Panic stops. Different class. The [Rust reference](https://doc.rust-lang.org/reference/behavior-considered-undefined.html) lists what counts as UB in unsafe code. Safe indexing is not on that list; it panics instead. +When Rust calls C (`extern "C"`), rustc trusts the C side ([FFI](https://doc.rust-lang.org/nomicon/ffi.html)). Wrong length, a “success” null, `from_raw_parts(ptr, len)`: rustc never measured `len`. Tests 1–2 are compile-time reject. Test 3 is runtime panic. Tests 4–5 are where the proof stops. -## C++23, sanitizers, Boost, crates +Panic / OOM are still bugs. They are not [`strcpy`](https://en.cppreference.com/w/c/string/byte/strcpy) UB. Safe indexing is not on the [UB list](https://doc.rust-lang.org/reference/behavior-considered-undefined.html); it panics. -The first tests used C arrays and `new`/`delete`. That is a fair “default C++ still lets you” demo. It is not a fair “C++ has no tools” demo. So I ran the same bugs again with **C++23**, **std::span**, **std::vector**, and [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer). Compilers: **g++ 13.3** and **clang++ 18.1.3**, `-std=c++23`. Rust: **rustc 1.93.1**. +## C++23 and sanitizers -**What C++23 did not change.** `std::span` is a pointer plus a length, like a Rust slice type, but `span[i]` does **not** check `i` in the default operator. I compiled this: +Default C++ still lets you `new`/`delete` and `v[i]`. C++ also has tools. I re-ran the bugs with **C++23**, `std::span`, `std::vector`, and [ASan](https://github.com/google/sanitizers/wiki/AddressSanitizer) (`g++` 13.3 / `clang++` 18.1.3, `-std=c++23`). -```cpp -#include -#include -#include -int main(int argc, char** argv) { - std::vector v{0, 0, 0, 0}; - std::span s = v; - int i = argc > 1 ? std::stoi(argv[1]) : 4; - s[i] = 42; - std::cout << "still running s[0]=" << s[0] << "\n"; -} -``` +`span[i]` does **not** check `i`. Same silent write past a vector of length 4: ```text $ g++ -std=c++23 -O0 -Wall -Wextra cxx23_span.cpp -o cxx23_span -$ clang++ -std=c++23 -O0 -Wall -Wextra cxx23_span.cpp -o cxx23_span_clang -# both: exit 0, no warning $ ./cxx23_span 4 still running s[0]=0 -$ ./cxx23_span_clang 4 -still running s[0]=0 v.size()=4 -# exit 0 -``` - -New standard. Same silent write past the vector. C++23 is not a borrow checker. - -**What a sanitizer did change.** Same source, extra flag: - -```text +# clang++ 18: same, exit 0 $ g++ -std=c++23 -O0 -Wall -Wextra -fsanitize=address cxx23_span.cpp -o cxx23_span_asan $ ./cxx23_span_asan 4 ERROR: AddressSanitizer: heap-buffer-overflow -WRITE of size 4 -SUMMARY: AddressSanitizer: heap-buffer-overflow ... in main -# abort, exit 1 - -$ clang++ -std=c++23 -O0 -Wall -Wextra -fsanitize=address cxx23_span.cpp -o cxx23_span_clang_asan -$ ./cxx23_span_clang_asan 4 -ERROR: AddressSanitizer: heap-buffer-overflow -SUMMARY: AddressSanitizer: heap-buffer-overflow ... in main -``` - -The `string_view` after `delete` program from test 1, still C++23: - -```text -$ g++ -std=c++23 -O0 -Wall -Wextra cxx23_uaf.cpp -o cxx23_uaf -$ ./cxx23_uaf -secret -# exit 0: printed freed memory - -$ g++ -std=c++23 -O0 -Wall -Wextra -fsanitize=address cxx23_uaf.cpp -o cxx23_uaf_asan -$ ./cxx23_uaf_asan -ERROR: AddressSanitizer: heap-use-after-free -SUMMARY: AddressSanitizer: heap-use-after-free ... fwrite -# abort, exit 1 -``` - -So: **C++23 + ASan caught both bugs at run time.** That is real. It is also optional. You must pass `-fsanitize=address`, take the slowdown, and **run the path**. ASan does not run on code you never execute. rustc’s check on `&s` and on `a[10]` happens at compile time with no extra flag. The variable-index panic happens on every run of that binary, debug or `-O`. - -**C++ already has a panic-shaped API.** [`std::vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) throws: - -```cpp -v.at(i) = 42; // i == 4 -``` - -```text -$ g++ -std=c++23 -O0 -Wall -Wextra cxx23_at.cpp -o cxx23_at -$ ./cxx23_at 4 -terminate called after throwing an instance of 'std::out_of_range' - what(): vector::_M_range_check: __n (which is 4) >= this->size() (which is 4) # abort ``` -That is closer to Rust `v[i]`. The catch: the **usual** C++ index is `v[i]` / `span[i]`, which does not throw. Rust’s usual index is the checked one. Defaults matter. - -**Boost vs crates.** C++ spent years putting Boost into `std`. [`boost::optional`](https://www.boost.org/doc/libs/release/libs/optional/doc/html/index.html) became [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional). Boost.Filesystem became [`std::filesystem`](https://en.cppreference.com/w/cpp/filesystem). Smart pointers, `span`, `string_view`: same story. This machine has Boost headers; I did not need them for the tests above because C++23 already has those types. What Boost still is: the leftover kitchen sink ([Asio](https://www.boost.org/doc/libs/release/doc/html/boost_asio.html), Spirit, uBLAS, …) until `std` or another library eats it. - -Rust does **not** need a Boost-the-project. [`std`](https://doc.rust-lang.org/std/) already has `Option`, `Result`, `Box` / `Rc` / `Arc`, `Vec`, slices, [`std::fs`](https://doc.rust-lang.org/std/fs/). The Boost-sized rest lives on [crates.io](https://crates.io/): [Tokio](https://tokio.rs/) is Asio, [serde](https://serde.rs/) is serialization, [nix](https://docs.rs/nix) is POSIX, [nom](https://docs.rs/nom) is parsers. Cargo pulls one version and records it in `Cargo.lock`. That is why “is there a Boost for Rust?” is the wrong question. There is an ecosystem. The cost is the same cost Boost always had: you must trust the crate, same as you must trust a Boost module or a C++ library. +C++23 `string_view` after `delete` printed `secret` (exit 0). With ASan: `heap-use-after-free`, abort. -None of that makes `span[i]` check bounds by default. Libraries do not replace the compiler’s default. +C++23 + ASan caught both **at run time**, if you pass the flag and **run the path**. rustc’s check on `&s` / `a[10]` is compile time with no extra flag. The variable-index panic is in every binary, debug or `-O`. -## The real difference is the default +[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) throws `out_of_range` — closer to Rust `v[i]`. The usual C++ spelling is still unchecked `v[i]` / `span[i]`. Rust’s usual spelling is the checked one. Unchecked Rust is `unsafe { *v.get_unchecked(i) }`. -C++ is not empty of safety tools. RAII, `unique_ptr`, `span`, `vector::at`, `optional`, atomics, sanitizers. The hole is there is **no one mandatory model**. `v[i]` is unchecked. `v.at(i)` checks. I compiled both. - -Rust’s normal `v[i]` is the checked one. The unchecked spelling is a different API and sits in `unsafe`: - -```rust -unsafe { *v.get_unchecked(i) } -``` - -The useful question is not “does C++ have a safe API?” It does. It is: **what happens if the programmer forgets?** That is the default. +Boost became a lot of `std`; the rest of the kitchen sink is crates.io on the Rust side. Trusting a crate is the same problem as trusting Boost. Neither makes `span[i]` check bounds by default. ## `unsafe` is a proof boundary -`unsafe` means: “compiler, trust me here.” It is not a confession that Rust failed. It is the door out of the proof. - -A common line: “Rust has `unsafe`, so it is C.” Too simple. The point is the hole is **named**, and you can keep it small. +`unsafe` means: “compiler, trust me here.” The hole is **named**, so you can keep it small. Callers stay in safe Rust; inside, you own the invariant. ```rust pub fn first_byte(data: &[u8]) -> u8 { @@ -1116,11 +821,7 @@ pub fn first_byte(data: &[u8]) -> u8 { } ``` -Callers stay in safe Rust. Inside, you own the invariant (`data` not empty). Ten lines of `unsafe` under a thousand lines of safe code is the design. Five thousand lines of `transmute`, raw pointers, and FFI is C with extra steps. - -Counting `unsafe` blocks is not a science score. The question is: **what invariant does this block establish, and who relies on it?** - -This is the first trick people send: +Ten lines of `unsafe` under a thousand lines of safe code is the design. Five thousand lines of `transmute` and FFI is C with extra steps. Count invariants, not blocks. ```rust pub fn as_static(s: &str) -> &'static str { @@ -1128,122 +829,23 @@ pub fn as_static(s: &str) -> &'static str { } ``` -`main` has no `unsafe`. The program can still use memory after it is freed. Why? rustc checks the **function type**, not the proof inside `unsafe`. `std` uses `unsafe` too, on purpose: hide the dangerous bit. The bad case is when that hiding is a lie. +`main` has no `unsafe`. The program can still use-after-free. rustc checks the **function type**, not the proof inside `unsafe`. `std` hides dangerous bits on purpose. A lie in that hiding is still a lie. -## Libraries and C code - -“Our crate has no `unsafe`” is not the full story. Your `Cargo.lock` may pull in other crates that do. `cargo audit` finds **known** security reports. It does not prove every library is correct. - -Calling C is the same idea with a C API instead of a crate name. `from_raw_parts(pointer, length)`: the length came from C. rustc never checked it. - -## A real CVE, two ways - -**uutils** is GNU coreutils rewritten in Rust (`ls`, `dd`, `cp`, …). Ubuntu 25.10 ships it. Canonical paid Zellic to review it. I want one pair of bugs in the form security people actually use: what happened, why the language allowed it, would **safe** Rust have stopped it, what still goes wrong. - -### CVE-2026-56392 (GNU `unexpand`): heap overflow - -**What happened.** Tab-stop count times element size wrapped. The allocator got a small buffer. The write loop used the old count. Heap overflow. [OSV](https://osv.dev/vulnerability/CVE-2026-56392). CERT Polska also lists [CVE-2026-56391](https://cert.pl/en/posts/2026/07/CVE-2026-56391/) (out-of-bounds read). - -**Why C allowed it.** Integer wrap plus `malloc` plus a write past the allocation is undefined behavior. The type system does not track “this length is the size I allocated.” - -**Would safe Rust prevent it?** The overflow-as-smash, usually yes: `Vec` length is the allocation; `v[i]` panics or you use `checked` / saturating math on purpose. A logic error that picks the **wrong** tab list is still possible. - -**What could still go wrong in Rust?** Panic as DoS. Wrong output. An `unsafe` wrapper around a C allocator with a length you computed yourself: you are back in C. - -### CVE-2026-35344 (uutils `dd`): ignored error - -**What happened.** `dd` hid a truncate failure with `.ok()`. [Advisory](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc). The process looked successful. Data was not what the user asked for. - -**Why “Rust” allowed it.** This is not a memory bug. `Result` is a value. `.ok()` throws the `Err` away. rustc does not know you needed that truncate. - -**Would safe Rust prevent it?** No. Memory stayed fine. The **program** was wrong. - -**What could still go wrong?** Same class as every `unwrap` / `.ok()` / ignored `io::Error`. Level 1 does not care. - -### CVE-2026-35359 (uutils `cp`): TOCTOU - -**What happened.** `cp` checked a path, then opened it without `O_NOFOLLOW`. Swap a symlink in the gap. [oss-security](https://www.openwall.com/lists/oss-security/2026/05/02/2). Ubuntu 26.04 still ships GNU `cp` / `mv` / `rm` for that reason. - -**Would safe Rust prevent it?** No. See [the TOCTOU test](#4-file-race-toctou). rustc compiled it. ASan was silent. - -Zellic’s public write-up through [Bugs Rust Won’t Catch](https://corrode.dev/blog/bugs-rust-wont-catch/) and Canonical’s [update](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773): mostly file races, permissions, “not the same as GNU,” ignored errors. They did **not** report classic overflow / UAF as the main pile. That is the rewrite working **and** the guarantee being smaller than the slide. - -I searched a 2026 tree. About two hundred `unsafe` hits in `src/`. One bad case: a BSD C function can return length 0 and a null pointer. The Rust wrapper only rejected negative length, then built a slice from null. That is Level 3: wrong C check, then `from_raw_parts`. +“Our crate has no `unsafe`” is incomplete: `Cargo.lock` may pull crates that do. `cargo audit` finds **known** advisories; it does not prove libraries correct. ## From borrow checking to LLVM -Safe Rust is only as strong as rustc. If rustc **accepts an invalid program**, the Level 1 guarantee does not hold for that program. The language spec said no. The implementation said yes. LLVM then optimizes as if the type were true. - -The borrow checker is not the last pass. +Safe Rust is only as strong as rustc. If rustc **accepts an invalid program**, the language said no and the implementation said yes. LLVM then optimizes as if the type were true. That is a miscompile of the *language*, not a random backend crash. ```text -Rust source - | - v -AST - | - v -HIR - | - v -Type checking - | - v -Borrow checking - | - v -MIR - | - v -MIR optimization - | - v -LLVM IR - | - v -Machine code +source → AST → HIR → type check → borrow check → MIR → LLVM IR → machine code ``` -Ownership is checked **before** LLVM. Some of it still shows up in IR. `&mut i32` is not “any C pointer.” For that lifetime, no other conflicting write. rustc can lower that as LLVM `noalias`. The optimizer may delete loads it thinks are impossible. - -If the frontend was **wrong**, the optimizer is still “correct” relative to the IR it was given. That is a miscompile of the *language*, not a random backend crash. - -```text -language rules - | - v -borrow checking - | - v -unsafe contracts - | - v -compiler correctness - | - v -optimizer correctness -``` - -The guarantee is not “the borrow checker is perfect.” It is: **the whole pipeline preserves the language’s safety rules.** - -| Layer | What can go wrong | -|---|---| -| Parser | Accepts / rejects the wrong syntax | -| Type checker | Wrong type judgment | -| Borrow checker | Accepts a bad lifetime or alias | -| MIR | Bad transform | -| MIR optimizer | Miscompiles valid Rust | -| LLVM lowering | Wrong IR | -| LLVM optimizer | Wrong transform (`noalias` on a lie) | -| Backend / linker | Wrong instruction or layout | +Ownership is checked **before** LLVM. `&mut` is not “any C pointer”; rustc can lower it as `noalias`. If rustc **wrongly** emits `&'static`, LLVM may treat that pointer as forever. A hole in lifetime rules is permission for the optimizer. -I used to ignore LLVM `noalias`. Then I saw what happens if rustc **wrongly** says a pointer lives forever (`&'static`). LLVM may treat that pointer as real. **Memory safety** (don’t smash the heap) is not the same as **memory-model rules** (what the optimizer may assume). `&mut` means “only I can write.” If those two stories disagree, even “safe” code can be compiled wrong. +The guarantee is not “the borrow checker is perfect.” It is: **the whole pipeline preserves the language’s safety rules.** Same numbered list as above: language → `unsafe` → FFI → libraries → **rustc** → **LLVM** → OS. ISSTA 2026 and #25860 sit on the rustc step. They are not a gotcha against `Vec`. -A pointer is not “just a number.” A hole in lifetime rules is permission for the optimizer. - -The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws the C++ side next to this. - -Question for compiler people: **if rustc incorrectly accepts invalid code, does the safety guarantee still hold?** No. That is why ISSTA 2026 and #25860 sit here. They are not a gotcha against `Vec`. They are a limit on Level 1. +The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws the C++ side. ## A 2026 research paper @@ -1349,22 +951,15 @@ I would not write: “Rust makes programs memory safe.” Too broad. I would wri **Safe Rust moves a large class of memory-safety errors from runtime into compile-time rules. `unsafe`, FFI, compiler bugs, OS interfaces, and logic errors stay outside that guarantee.** -Less marketable. More useful. - -The engineering question is not “is Rust safe?” It is: **which invariants does rustc enforce for this code, and where does the programmer prove the rest?** - -```text -Safe Rust: ownership, borrows, lifetimes, bounds, data-race rules - -> compiler-enforced +:::tip What exactly does Rust guarantee? +**In safe code (if rustc is sound), Rust is built to stop:** no use-after-free, no dangling references, no double-free, no data races, bounds-checked indexing, ownership/lifetime consistency. -unsafe / FFI: raw pointers, C lengths, get_unchecked - -> you +**It does not guarantee:** correct business logic, race-free filesystem operations, correct FFI contracts, absence of `unsafe` bugs, absence of library / rustc / LLVM bugs, absence of DoS / panic / OOM. +::: -rustc -> MIR -> LLVM -> backend - -> compiler correctness -``` +The interesting difference between Rust and C++ is not whether safety tools exist. It is **what happens when the programmer forgets to use them.** -Rust did not delete the need for correctness. It moved a big piece of it into the type system. That is the achievement. +Rust did not delete the need for correctness. It moved a big piece of it into the type system. Memory safety is a floor, not the whole building. ## Limits @@ -1374,6 +969,8 @@ C and Rust can live together. People are still the expensive part. Checking more ## References +Claims in the article point here: rustc soundness and #25860 (2–9); uutils / GNU CVEs and TOCTOU (10–13); ASan, `span`, `at` (14). + 1. [Why are some people against the Rust-Lang?](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906), May 2023. 2. [rust-lang/rust#25860](https://github.com/rust-lang/rust/issues/25860). 3. [PR #156077](https://github.com/rust-lang/rust/pull/156077) (closed, did not land). From 1d758e6b96e19095bb93e5e31c345b71a4758ae5 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:19:54 +0530 Subject: [PATCH 09/14] Fold rustc limits in earlier and hide repeated compiler logs. Add an unsafe/FFI checklist, note span::at vs operator[] and LLVM-elided bounds checks, and cite references inline. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 188 +++++++------------ 1 file changed, 73 insertions(+), 115 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index c4135c66..26efd8d5 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -64,11 +64,13 @@ import Head from '@docusaurus/Head'; :::note Related: [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [How rustc compiles vs C++](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) + +This piece is for people who already know what a use-after-free is and care where rustc vs LLVM vs the OS sit. The table below is the whole argument. Compiler logs live in collapsible blocks. ::: People say **Rust is memory safe**. I wanted the compiler version of that sentence, not the slide. So I compiled the same bugs with rustc 1.93.1, gcc/g++ 13.3, and clang 18, then asked where the proof stops: language, rustc, LLVM, or the OS. -C++ already has `unique_ptr`, `span`, `v.at(i)`, sanitizers. The interesting difference is **what happens when the programmer forgets to use them.** Rust’s normal `v[i]` is checked. C++’s normal `v[i]` is not. +C++ already has `unique_ptr`, `span`, `v.at(i)`, sanitizers. The interesting difference is **what happens when the programmer forgets to use them.** Rust’s normal `v[i]` is checked. C++’s normal `v[i]` is not. Safety-critical C++ often uses `span::at` / GSL / custom wrappers; those are still a choice, not the language default. On the Rust side, LLVM may **elide** a bounds check it can prove, and `no_std` still panics (or `abort`) unless you wrote `get_unchecked` — the check is not a `std`-only extra. The claim is real. It is also smaller than “Rust is memory safe.” @@ -99,8 +101,8 @@ The claim is real. It is also smaller than “Rust is memory safe.” Evidence is below. Three different index stories, say them once: 1. **Constant index rustc can see** → compile-time rejection. No binary. -2. **Runtime index** → rustc emits a bounds check → **panic** (defined). Not C undefined behavior. -3. **`unsafe { *v.get_unchecked(i) }`** → you hold the proof. Broken invariant is UB. +2. **Runtime index** → rustc emits a bounds check → **panic** (defined). Not C undefined behavior. A later LLVM pass may drop the check if it proves `i` in range. +3. **`unsafe { *v.get_unchecked(i) }`** → you hold the proof. Broken invariant is UB. Use this in a hot loop only after you have a local proof (length already tested, iterator already in range). ## Table of Contents @@ -113,11 +115,11 @@ Evidence is below. Three different index stories, say them once: - [C++23 and sanitizers](#c23-and-sanitizers) - [`unsafe` is a proof boundary](#unsafe-is-a-proof-boundary) - [From borrow checking to LLVM](#from-borrow-checking-to-llvm) -- [A 2026 research paper](#a-2026-research-paper) -- [Bug #25860, which I compiled](#bug-25860-which-i-compiled) +- [Limits of rustc](#limits-of-rustc) +- [Checklist](#checklist) - [What you pay for the checks](#what-you-pay-for-the-checks) - [The claim I would actually make](#the-claim-i-would-actually-make) -- [Limits](#limits) +- [How I ran this](#how-i-ran-this) - [References](#references) ## The short answer @@ -147,9 +149,10 @@ error[E0515]: cannot return reference to local variable `s` No binary. That reject is the good outcome. -In C you can `free(p)` then `printf("%s", p)`: gcc warns `-Wuse-after-free` and still links. **clang 18.1.3** with `-Wall -Wextra` said **nothing** and still linked. In C++ you can keep a `string_view` after `delete`: g++ 13.3 and clang++ 18 both said nothing and still linked. Those programs can crash, print garbage, or read data an attacker put in the reused heap. +In C you can `free(p)` then `printf("%s", p)`: gcc emits `-Wuse-after-free` and still produces a binary. **clang 18.1.3** with `-Wall -Wextra` produced a binary with no warning. In C++ you can keep a `string_view` after `delete`: g++ 13.3 and clang++ 18 both produced a binary with no warning. Those programs can crash, print garbage, or read data an attacker put in the reused heap. -Same C, with a sanitizer: +

+ASan on the same C/C++ sources (opt-in rebuild + run) ```text $ gcc -O0 -Wall -Wextra -fsanitize=address uaf.c -o uaf_c_asan @@ -157,14 +160,11 @@ $ ./uaf_c_asan ERROR: AddressSanitizer: heap-use-after-free SUMMARY: AddressSanitizer: heap-use-after-free ... in printf_common # abort, exit 1 - -$ clang -O0 -Wall -Wextra -fsanitize=address uaf.c -o uaf_clang_asan -$ ./uaf_clang_asan -ERROR: AddressSanitizer: heap-use-after-free -SUMMARY: AddressSanitizer: heap-use-after-free ... in printf_common ``` -C++ `string_view` after `delete`, ASan: `heap-use-after-free` in `fwrite`, abort. So yes: **a sanitizer can report the same class of bug Rust refused.** You had to rebuild with `-fsanitize=address` and actually run `main`. rustc never let a binary out. +clang 18 with the same flag: same abort. C++ `string_view` after `delete` + ASan: `heap-use-after-free` in `fwrite`, abort. A sanitizer can report the same class of bug rustc refused. You had to rebuild with `-fsanitize=address` [[14]](#references) and actually run `main`. rustc never emitted a binary. + +
**Example 2.** This program makes an array of four zeros, then writes index 10: @@ -175,35 +175,24 @@ fn main() { } ``` -What it is doing: valid indexes are 0, 1, 2, 3. Index 10 is six slots past the end. In C and C++ that write is undefined behavior: smash the stack, overwrite a return address, or look fine until it does not. gcc and g++ 13.3 with `-Wall -Wextra` built it with **no diagnostic**. clang and clang++ 18 warned `-Warray-bounds` and **still linked**. - -What rustc did (default): +What it is doing: valid indexes are 0, 1, 2, 3. Index 10 is six slots past the end. In C and C++ that write is undefined behavior: smash the stack, overwrite a return address, or look fine until it does not. gcc and g++ 13.3 with `-Wall -Wextra` built it with **no diagnostic**. clang and clang++ 18 warned `-Warray-bounds` and still produced a binary. -```text -error: this operation will panic at runtime - --> oob.rs:3:5 - | -3 | a[10] = 42; - | ^^^^^ index out of bounds: the length is 4 but the index is 10 - | - = note: `#[deny(unconditional_panic)]` on by default -``` +What rustc did (default): compile error, `deny(unconditional_panic)`, no binary. -No binary. +gcc/g++ 13.3 with `-Wall -Wextra` produced a binary with no diagnostic. clang/clang++ 18 warned `-Warray-bounds` and still produced a binary. -Same C with UBSan (ASan alone did not print a clean stack-overflow report on this tiny `int a[4]` in my run; UBSan did): +
+UBSan on the same C source ```text $ gcc -O0 -Wall -Wextra -fsanitize=undefined oob.c -o oob_ubsan $ ./oob_ubsan oob.c:3:6: runtime error: index 10 out of bounds for type 'int [4]' - -$ clang -O0 -Wall -Wextra -fsanitize=undefined oob.c -o oob_clang_ubsan -# also -Warray-bounds at compile time, then: -oob.c:3:5: runtime error: index 10 out of bounds for type 'int[4]' ``` -Again: the sanitizer can name the same bug. Default gcc still shipped a binary. Default rustc did not. +ASan alone did not print a clean report on this tiny stack `int a[4]` in my run; UBSan did. Default gcc still shipped a binary. Default rustc did not. + +
**Example 3.** Constant `a[10]` is the easy case. rustc can see the number. Now `i` comes from the command line, so the compiler cannot reject it up front. @@ -229,54 +218,26 @@ index out of bounds: the len is 4 but the index is 4 # exit 101: never prints "still running" ``` -Same with `rustc -O`. Bounds checks stay in. Valid index `0` prints `still running, a[0]=42`. +Same with `rustc -O`. Bounds checks stay unless LLVM can prove `i` in range. Valid index `0` prints `still running, a[0]=42`. -The C side, same idea: `i` from argv, array of 4, a neighbor `flag` sitting right after it: +C: `i` from argv, array of 4, neighbor `flag`. `./slice_i_c 4` printed `still running` and `flag` went from 7 to 42 (gcc and clang, `-O0 -Wall -Wextra`). Default UBSan printed the OOB message and **continued**; ASan often misses this intra-object overflow. `-fno-sanitize-recover=undefined` aborts — extra flag. Rust panic needed none. -```c -struct Box { - int a[4]; - int flag; -}; -int i = atoi(argv[1]); /* we passed 4 */ -b.a[i] = 42; -printf("still running a[0]=%d flag=%d\n", b.a[0], b.flag); -``` +
+C smash + default UBSan recover ```text -$ gcc -O0 -Wall -Wextra slice_i.c -o slice_i_c # exit 0, no warning +$ gcc -O0 -Wall -Wextra slice_i.c -o slice_i_c $ ./slice_i_c 4 still running a[0]=0 flag=42 -# exit 0 - -$ clang -O0 -Wall -Wextra slice_i.c -o slice_i_clang -$ ./slice_i_clang 4 -still running a[0]=0 flag=42 -# exit 0: same smash -``` -`flag` started as `7`. After `a[4] = 42` it is `42`. The program kept going. That is the unsafe failure. - -Now the sanitizer: this is the honest part. ASan + UBSan, **defaults**: - -```text $ gcc -O0 -Wall -Wextra -fsanitize=address,undefined slice_i.c -o slice_i_san $ ./slice_i_san 4 slice_i.c:12:8: runtime error: index 4 out of bounds for type 'int [4]' still running a[0]=0 flag=42 # exit 0 - -$ clang -O0 -Wall -Wextra -fsanitize=address,undefined slice_i.c -o slice_i_clang_san -$ ./slice_i_clang_san 4 -slice_i.c:12:5: runtime error: index 4 out of bounds for type 'int[4]' -SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ... -still running a[0]=0 flag=42 -# exit 0 ``` -UBSan **printed** the same “index out of bounds” story Rust panics with. Then the program **kept running** and `flag` was still smashed. Default UBSan recovers. ASan did not stop this one: `a[4]` is the next field in the same `struct`, an intra-object overflow sanitizers often miss. - -If I add `-fno-sanitize-recover=undefined`, UBSan aborts and does not print `still running`. That flag is extra. Rust’s panic on `a[i]` needed no extra flag. +
Sanitizers can name the same bugs. They are a flag you pass and a path you must run. Safe Rust’s reject/panic is the default. C does not mark `a[i] = 42` as `unsafe`. rustc bugs and FFI still sit outside that default; those sections come later. @@ -298,7 +259,7 @@ This is the compiler article’s spine. **Memory-safety as a language model** is 2. **`unsafe`** — you assume the invariant. 3. **FFI** — rustc trusts C lengths, pointers, and ABI. 4. **Library implementation** — `std` and crates contain `unsafe`; `Cargo.lock` is in the TCB. -5. **rustc** — a soundness bug accepts code the language forbids. That is not “Rust lied”; the *implementation* did. +5. **rustc** — a soundness bug accepts code the language forbids. That is not “Rust lied”; the *implementation* did. [Limits of rustc](#limits-of-rustc) is that step: ISSTA 2026 [[5]](#references) and [#25860](https://github.com/rust-lang/rust/issues/25860) [[2]](#references). 6. **LLVM** — optimizes IR it was given (`noalias` on a lie is still “correct” LLVM). 7. **OS / hardware** — TOCTOU, `mmap`, permissions, cosmic rays. @@ -441,6 +402,11 @@ clang -O0 -Wall -Wextra slice_i.c -o slice_i_clang ./slice_i_clang 4 # still running, flag smashed ``` +The UAF / constant-OOB / runtime-index sources match [the short answer](#the-short-answer). Full listings: + +
+Full sources and compiler logs for tests 1–3 + ### 1. Use memory after free @@ -467,7 +433,7 @@ uaf.c:7:5: warning: pointer ‘p’ used after ‘free’ [-Wuse-after-free] uaf.c:6:5: note: call to ‘free’ here 6 | free(p); | ^~~~~~~ -# exit code 0: you still get a binary +# exit code 0: gcc still produces a binary ``` ```text @@ -476,7 +442,7 @@ $ clang -Wall -Wextra uaf.c -o uaf_clang # exit code 0 ``` -**Why that is bad.** `free` gave the heap block back. `printf` still reads it. The bytes may be garbage, may crash, or may be data an attacker put there after reuse. gcc saw the bug and **still linked**. clang 18 with `-Wall -Wextra` did not even warn. A warning is not a stop. +**Why that is bad.** `free` returned the heap block. `printf` still reads it. The bytes may be garbage, may crash, or may be data an attacker put there after reuse. gcc diagnosed it (`-Wuse-after-free`) and still produced a binary. clang 18 with `-Wall -Wextra` did not warn. A warning is not a reject. @@ -529,7 +495,7 @@ error: aborting due to 1 previous error -What surprised me was C++, not Rust. g++ and clang++ made a binary and said nothing. gcc at least warned, then still linked. clang 18 did not warn on the `free` then `printf` case. Tools like AddressSanitizer can catch the C/C++ bugs **if you turn them on**. I did not turn them on for the default builds. The slogan is about the normal build, not the special test build. +What surprised me was C++, not Rust. g++ and clang++ produced a binary with no diagnostic. gcc warned, then still produced a binary. clang 18 did not warn on `free` then `printf`. AddressSanitizer can catch these **if you turn it on**. The slogan is about the default build. ### 2. Write past the array @@ -553,10 +519,10 @@ $ clang -Wall -Wextra oob.c -o oob_clang oob.c:3:5: warning: array index 10 is past the end of the array (that has type 'int[4]') [-Warray-bounds] 3 | a[10] = 42; | ^ ~~ -# exit code 0: you still get a binary +# exit code 0: gcc still produces a binary ``` -**Why that is bad.** The array has four `int`s. Index 10 is six slots past the end. In C that is undefined behavior: smash the stack, overwrite a return address, or “work” until it does not. gcc 13.3 with `-Wall -Wextra` still built it with no warning. clang 18 warned, then **still linked**. rustc refused. +**Why that is bad.** The array has four `int`s. Index 10 is six slots past the end. In C that is undefined behavior: smash the stack, overwrite a return address, or “work” until it does not. gcc 13.3 with `-Wall -Wextra` still built it with no warning. clang 18 warned, then still produced a binary. rustc refused. @@ -683,6 +649,8 @@ index out of bounds: the len is 4 but the index is 4 +
+ ### 4. File race (TOCTOU) You check a file path. Then someone swaps the file. Then you open the path. The compiler does not see that. That bug has a name: [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use) (time-of-check to time-of-use). Rust [`std::fs`](https://doc.rust-lang.org/std/fs/) takes paths, like many C programs. Two syscalls on the same path string are two lookups. The file can change in between. @@ -776,7 +744,7 @@ use: read "secret" Rust can compile the program, ASan can stay silent, and the program can still use the wrong file. Memory safety is not general security. -The production cousin is [CVE-2026-35359](https://www.openwall.com/lists/oss-security/2026/05/02/2) in Ubuntu’s Rust [uutils](https://github.com/uutils/coreutils) `cp`: check a path, open without `O_NOFOLLOW`, swap a symlink. Canonical / [Zellic](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf) found mostly file races and ignored errors, not a pile of UAF. GNU still shipped a heap overflow: [CVE-2026-56392](https://osv.dev/vulnerability/CVE-2026-56392). Different class, both real. uutils `dd` [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) hid a truncate failure with `.ok()`: memory fine, program wrong. +The production cousin is [CVE-2026-35359](https://www.openwall.com/lists/oss-security/2026/05/02/2) [[11]](#references) in Ubuntu’s Rust [uutils](https://github.com/uutils/coreutils) `cp`. Canonical / [Zellic](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf) found mostly file races and ignored errors, not a pile of UAF. GNU still shipped a heap overflow: [CVE-2026-56392](https://osv.dev/vulnerability/CVE-2026-56392) [[12]](#references). uutils `dd` [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc) hid a truncate failure with `.ok()`. Fix for the experiment: open **once**, then `fstat` / operate on the **file descriptor**, or `O_NOFOLLOW`. @@ -790,7 +758,7 @@ Panic / OOM are still bugs. They are not [`strcpy`](https://en.cppreference.com/ Default C++ still lets you `new`/`delete` and `v[i]`. C++ also has tools. I re-ran the bugs with **C++23**, `std::span`, `std::vector`, and [ASan](https://github.com/google/sanitizers/wiki/AddressSanitizer) (`g++` 13.3 / `clang++` 18.1.3, `-std=c++23`). -`span[i]` does **not** check `i`. Same silent write past a vector of length 4: +`operator[]` on `std::span` does **not** check `i`. `span::at` does (throws), same split as `vector`. Safety-critical code often uses `at()`, GSL `span`, or a project wrapper. Default `s[i]` on a length-4 vector still wrote past the end in my C++23 build: ```text $ g++ -std=c++23 -O0 -Wall -Wextra cxx23_span.cpp -o cxx23_span @@ -807,7 +775,7 @@ C++23 `string_view` after `delete` printed `secret` (exit 0). With ASan: `heap-u C++23 + ASan caught both **at run time**, if you pass the flag and **run the path**. rustc’s check on `&s` / `a[10]` is compile time with no extra flag. The variable-index panic is in every binary, debug or `-O`. -[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) throws `out_of_range` — closer to Rust `v[i]`. The usual C++ spelling is still unchecked `v[i]` / `span[i]`. Rust’s usual spelling is the checked one. Unchecked Rust is `unsafe { *v.get_unchecked(i) }`. +[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) / `span::at` throw `out_of_range` — closer to Rust `v[i]`. The usual C++ spelling is still unchecked `v[i]` / `span[i]`. Rust’s usual spelling is the checked one; LLVM may drop the check when it proves the index. Unchecked Rust is `unsafe { *v.get_unchecked(i) }`. Boost became a lot of `std`; the rest of the kitchen sink is crates.io on the Rust side. Trusting a crate is the same problem as trusting Boost. Neither makes `span[i]` check bounds by default. @@ -843,47 +811,23 @@ source → AST → HIR → type check → borrow check → MIR → LLVM IR → m Ownership is checked **before** LLVM. `&mut` is not “any C pointer”; rustc can lower it as `noalias`. If rustc **wrongly** emits `&'static`, LLVM may treat that pointer as forever. A hole in lifetime rules is permission for the optimizer. -The guarantee is not “the borrow checker is perfect.” It is: **the whole pipeline preserves the language’s safety rules.** Same numbered list as above: language → `unsafe` → FFI → libraries → **rustc** → **LLVM** → OS. ISSTA 2026 and #25860 sit on the rustc step. They are not a gotcha against `Vec`. +The guarantee is not “the borrow checker is perfect.” It is: **the whole pipeline preserves the language’s safety rules.** rustc and LLVM are two later steps on that list. The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws the C++ side. -## A 2026 research paper - -Yusung Sim, Sukyoung Ryu (KAIST), Jaemin Hong (UNIST) wrote [Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) for ISSTA 2026. Extra files: [Zenodo](https://doi.org/10.5281/zenodo.20698055). - -This paper is **not** “Rust apps have bugs.” It is “rustc sometimes accepts programs it should reject.” - -Three different compiler bugs: - -1. rustc **crashes**: annoying, not a memory smash in your app -2. rustc **rejects good code**: also annoying -3. rustc **accepts bad code**: this is the soundness bug. This one can hide a use-after-free behind `cargo build` with no `unsafe` +## Limits of rustc -Another paper (Liu et al., OOPSLA 2025) counted many kinds of rustc bugs. This ISSTA paper looks only at type (3), and compares with Liu. +This is step 5 in [where the language guarantee ends](#where-the-language-guarantee-ends). The language model can be sound while **this rustc** accepts a program it should reject. LLVM then treats the lie as IR truth. -How they built the list: GitHub issues from Jan 2022 to Sep 2025 about types (969) → bug / unsound labels (320) → read by hand (**23**). The short abstract says 23. I almost stopped there. The extra files add 7 more from Liu. Final study set: **30**. I wish the abstract said both numbers. +Yusung Sim, Sukyoung Ryu (KAIST), Jaemin Hong (UNIST), [Rust's Type Checker Implementation is Unsound](https://conf.researchr.org/details/issta-2026/issta-2026-research-papers/129/Rust-s-Type-Checker-Implementation-is-Unsound-An-Empirical-Study-on-Soundness-Bugs-i) (ISSTA 2026) [[5]](#references), artifact [[6]](#references). Not “Rust apps have bugs.” It is “rustc sometimes accepts programs it should reject.” -What they found, in simple words: +Three compiler-bug kinds: crash; reject good code; **accept bad code** (soundness). The last can hide UAF behind `cargo build` with no `unsafe`. Liu et al. (OOPSLA 2025) [[7]](#references) counted rustc bugs more broadly; ISSTA looks at type (3). Study set: abstract **23**, artifact **30** (plus 7 from Liu). -- Some of these bugs (often “implied bounds” or trait objects) can break memory safety. -- Hard cases are associated types and lifetimes mixed with traits: not `Vec` indexing. -- Many bugs were there from the day the feature shipped. Issue #25860 (2015) is the long example, even though it is older than their 2022–2025 window. -- **[Miri](https://github.com/rust-lang/miri)** can catch the ones that blow up at run time. Other formal tools (Chalk, a-mir-formality) are not ready as a full test of rustc. -- The official docs are often not precise enough to use as an automatic test. +Hard cases: implied bounds, trait objects, associated types — not `Vec` indexing. [#25860](https://github.com/rust-lang/rust/issues/25860) (2015) [[2]](#references) is the long example. [Miri](https://github.com/rust-lang/miri) [[9]](#references) can catch some that blow up at run time. The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) lists #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). C and C++ also lack a full machine spec of “must reject.” Rust’s short sentence needs rustc to be right. -Why #25860 can stay open for years: if the rule is not written as a machine-checkable test, you cannot fail rustc with a spec. You fail it with a program plus a human saying “this should not compile.” That is slow. +### The file I compiled (#25860) -The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) already lists this family: #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). - -C and C++ also do not have a full machine spec of “this must be rejected.” I am not picking on Rust for that. The difference is the **claim**. Rust’s short sentence needs rustc to be right. If tests cannot decide the edge, you have a team process (issues, types team, new solver), not a finished proof. - -Normal `HashMap` code is not this set. The set is also not empty. Next is the file I compiled. - -## Bug #25860, which I compiled - -[#25860](https://github.com/rust-lang/rust/issues/25860) has been open since May 2015. A real fix is waiting on bigger type-system work. [PR #156077](https://github.com/rust-lang/rust/pull/156077) in May 2026 was closed. It did not even build rustc. - -The [cve-rs](https://github.com/Speykious/cve-rs) example uses **zero** `unsafe`. A helper that is fine on its own: +[#25860](https://github.com/rust-lang/rust/issues/25860) is still open. [PR #156077](https://github.com/rust-lang/rust/pull/156077) [[3]](#references) (May 2026) closed without landing. [cve-rs](https://github.com/Speykious/cve-rs) [[4]](#references) uses **zero** `unsafe`: ```rust fn lifetime_translator<'a, 'b, T: ?Sized>( @@ -905,9 +849,27 @@ pub fn as_static(x: &T) -> &'static T { } ``` -I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. That is when “if it compiled, rustc proved it” died for me. Not for normal `Vec` code. For rustc. +I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. Normal `Vec` code is not this file. This is a rustc limit, not a `Vec` gotcha. + +## Checklist + +**`unsafe` block (write this in a comment above the block):** -Normal app code does not look like this. If you start a tools argument with this file, people will say “that is a compiler bug.” They are right. Start with docs search if that is your point. I keep this file because I ran it. +1. What invariant am I asserting? (non-null, aligned, `len` is the allocation, no alias with `&mut`, lifetime not `'static` unless it really is) +2. Who established it — this function, the caller, or C? +3. What would make it false on the next line? +4. Can Miri run this path in CI? + +**`get_unchecked`:** only after a local proof (`i < v.len()`, or an iterator that already walked the slice). If the proof is “I think the loop is fine,” keep `v[i]`. + +**FFI / C++ interop:** + +- Treat every `extern "C"` length and pointer as untrusted until you copy into a `Vec` / checked slice. +- Do not `from_raw_parts` on a C “success” that can be null + len 0 unless the C API documents that as empty. +- Prefer owning the allocation on one side. If C frees, Rust must not `Drop` the same bytes. +- On the C++ side: `unique_ptr` / `span::at` at the boundary; sanitizers on the C++ test binary, not only on the Rust crate. + +**Filesystem:** open once; `fstat` / operate on the fd; `O_NOFOLLOW` if the path must not be a symlink. ## What you pay for the checks @@ -921,11 +883,7 @@ The guarantee is not free. This is the bill I actually hit. - **Layout control.** Packed structs, custom allocators, MMIO: you will touch `unsafe` or stay in C. - **No std lending iterator.** A standard `Iterator` cannot yield a borrow from inside itself. Other crates exist ([rust-streaming](https://github.com/emk/rust-streaming)). -Around 2015 some people said: skip the borrow-checker fight, look at the tools. serde, `impl Trait`, `cargo install` got better. The three bullets above (docs search, compile wait, lending iterator) did not vanish. - -A May 2023 [forum thread](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) asked why people dislike Rust. Fair replies: a tiny part of a kernel is special CPU instructions; Rust and C can live together; nobody will rewrite a billion lines of old C. In 2026 some Linux kernel code is Rust, most is still C. - -Docs search and compile wait are one argument. #25860 is another. Do not mix them. +A May 2023 [forum thread](https://users.rust-lang.org/t/why-are-some-people-against-the-rust-lang/93906) [[1]](#references): special CPU ops stay in C; mixed trees are normal; nobody rewrites a billion lines. Docs search and compile wait are one argument. #25860 is another. ## The claim I would actually make From 546d8024a55465797f4116ea8c17c886f80e36d4 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:20:12 +0530 Subject: [PATCH 10/14] Rename the experiment-scope section so it does not collide with Limits of rustc. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index 26efd8d5..de7474eb 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -919,7 +919,7 @@ The interesting difference between Rust and C++ is not whether safety tools exis Rust did not delete the need for correctness. It moved a big piece of it into the type system. Memory safety is a floor, not the whole building. -## Limits +## How I ran this The small C/C++/Rust programs and #25860 were run on rustc 1.93.1, gcc/g++ 13.3, and clang/clang++ 18.1.3 on one machine. C++23 tests used `-std=c++23`; ASan used `-fsanitize=address`. #25860 is still open. ISSTA numbers come from the public abstract and artifact; I did not invent extra stats. uutils notes come from Canonical’s 2026 post, the Zellic PDF, and oss-security, not “every Rust CLI is clean.” Docs search and compile speed change every release. From d02b0b542a24e54d07462c489eb1d7dbd1d4f4dc Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:23:26 +0530 Subject: [PATCH 11/14] Add a TL;DR, merge the UAF walkthrough, and put the checklist after unsafe. Spell out that --release does not drop bounds checks unless LLVM proves the index, and bridge from the pipeline into the rustc soundness example. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 127 ++++++++----------- 1 file changed, 52 insertions(+), 75 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index de7474eb..c59003fa 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -62,6 +62,10 @@ import Head from '@docusaurus/Head'; # Rust Claims, a Reality Check: What rustc Proves, and Where the Proof Stops +:::tip In short +Safe Rust rejects use-after-free, constant out-of-bounds, and data races at compile time. Runtime out-of-bounds **panics by default** (`debug` and `--release`), unless LLVM can *prove* the index is in range — that elision does not weaken the rule. C and C++ need sanitizers or wrappers (`v.at(i)`, `span::at`) for the same class of bug; those are opt-in. `unsafe`, FFI, rustc/LLVM bugs, and logic errors sit outside that bubble. +::: + :::note Related: [Rust vs Modern C++](/docs/articles/rust-vs-modern-cpp-memory-safety-beyond-the-hype) · [How rustc compiles vs C++](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) @@ -70,7 +74,9 @@ This piece is for people who already know what a use-after-free is and care wher People say **Rust is memory safe**. I wanted the compiler version of that sentence, not the slide. So I compiled the same bugs with rustc 1.93.1, gcc/g++ 13.3, and clang 18, then asked where the proof stops: language, rustc, LLVM, or the OS. -C++ already has `unique_ptr`, `span`, `v.at(i)`, sanitizers. The interesting difference is **what happens when the programmer forgets to use them.** Rust’s normal `v[i]` is checked. C++’s normal `v[i]` is not. Safety-critical C++ often uses `span::at` / GSL / custom wrappers; those are still a choice, not the language default. On the Rust side, LLVM may **elide** a bounds check it can prove, and `no_std` still panics (or `abort`) unless you wrote `get_unchecked` — the check is not a `std`-only extra. +**The default index is the comparison.** In safe Rust, `v[i]` is the checked operation. In C++, `v[i]` / `span[i]` is the unchecked one; `v.at(i)` / `span::at` / GSL are extra. Safety-critical C++ often uses those extras. They remain a choice. `#![no_std]` still has the check in `core` unless you wrote `get_unchecked`. + +C++ already has `unique_ptr`, `span`, sanitizers. The interesting difference is **what happens when the programmer forgets to use them.** The claim is real. It is also smaller than “Rust is memory safe.” @@ -101,22 +107,21 @@ The claim is real. It is also smaller than “Rust is memory safe.” Evidence is below. Three different index stories, say them once: 1. **Constant index rustc can see** → compile-time rejection. No binary. -2. **Runtime index** → rustc emits a bounds check → **panic** (defined). Not C undefined behavior. A later LLVM pass may drop the check if it proves `i` in range. +2. **Runtime index** → rustc emits a bounds check → **panic** (defined). Not C undefined behavior. `--release` does **not** strip that check. LLVM may drop it only when it can **statically prove** `i` is in range. That is proving the access safe, not relaxing the language. 3. **`unsafe { *v.get_unchecked(i) }`** → you hold the proof. Broken invariant is UB. Use this in a hot loop only after you have a local proof (length already tested, iterator already in range). ## Table of Contents - [The short answer](#the-short-answer) - [Three levels of Rust safety](#three-levels-of-rust-safety) -- [What the borrow checker actually proves](#what-the-borrow-checker-actually-proves) -- [The same bug in C, C++, and Rust](#the-same-bug-in-c-c-and-rust) +- [Ownership: static proof vs leftover pointer](#ownership-static-proof-vs-leftover-pointer) - [Data races](#data-races) - [What I compiled](#what-i-compiled) - [C++23 and sanitizers](#c23-and-sanitizers) - [`unsafe` is a proof boundary](#unsafe-is-a-proof-boundary) +- [Checklist](#checklist) - [From borrow checking to LLVM](#from-borrow-checking-to-llvm) - [Limits of rustc](#limits-of-rustc) -- [Checklist](#checklist) - [What you pay for the checks](#what-you-pay-for-the-checks) - [The claim I would actually make](#the-claim-i-would-actually-make) - [How I ran this](#how-i-ran-this) @@ -179,8 +184,6 @@ What it is doing: valid indexes are 0, 1, 2, 3. Index 10 is six slots past the e What rustc did (default): compile error, `deny(unconditional_panic)`, no binary. -gcc/g++ 13.3 with `-Wall -Wextra` produced a binary with no diagnostic. clang/clang++ 18 warned `-Warray-bounds` and still produced a binary. -
UBSan on the same C source @@ -218,7 +221,7 @@ index out of bounds: the len is 4 but the index is 4 # exit 101: never prints "still running" ``` -Same with `rustc -O`. Bounds checks stay unless LLVM can prove `i` in range. Valid index `0` prints `still running, a[0]=42`. +Same with `rustc -O` / `--release`. The panic stayed. LLVM did not remove the check: it could not prove `i` in range. Valid index `0` prints `still running, a[0]=42`. C: `i` from argv, array of 4, neighbor `flag`. `./slice_i_c 4` printed `still running` and `flag` went from 7 to 42 (gcc and clang, `-O0 -Wall -Wextra`). Default UBSan printed the OOB message and **continued**; ASan often misses this intra-object overflow. `-fno-sanitize-recover=undefined` aborts — extra flag. Rust panic needed none. @@ -265,30 +268,11 @@ This is the compiler article’s spine. **Memory-safety as a language model** is The rest of the article fills that list with programs. -## What the borrow checker actually proves - -People say the borrow checker “prevents memory bugs.” True, and incomplete. It does **not** watch machine code. It checks ownership, lifetimes, and aliases **before** LLVM. - -```rust -fn use_string() { - let s = String::from("hello"); - let r = &s; - println!("{}", r); -} -``` - -`s` owns the heap `String`. `r` only borrows. rustc’s rule is: **owner lives at least as long as the borrow.** +## Ownership: static proof vs leftover pointer -Move the owner first: +Two different questions, one hole. -```rust -fn example() { - let s = String::from("hello"); - let r = &s; - drop(s); - println!("{}", r); -} -``` +**Borrow checker = static proof before a binary exists.** It does not watch machine code. It checks ownership, lifetimes, and aliases **before** LLVM. Rule: the owner lives at least as long as the borrow. ```text s owns memory @@ -297,18 +281,12 @@ s owns memory | +---- s is destroyed | - X r is still alive + X r is still alive → rustc reject (Example 1, E0515) ``` -I compiled that shape as [Example 1](#the-short-answer) (`&s` returned from the function). rustc: `E0515`. No binary. - -ASan asks: “did this **run** access dead memory?” The borrow checker asks: “can this program even **name** that relationship?” Sanitizer: observe an execution. Borrow checker: reject the program. +ASan asks: “did this **run** access dead memory?” The borrow checker asks: “can this program even **name** that relationship?” -Safe `v[100]` on a length-3 `Vec` is **not** C undefined behavior. rustc emits a bounds check; miss → **panic** (defined stop). C `v[100]` on `int v[3]` is UB: the optimizer may assume it never happens. `unsafe` is the second Rust path: broken contract → UB. - -## The same bug in C, C++, and Rust - -One hole, three spellings. Allocate an `int`, free it, write through the old pointer. +**Same hole if the compiler emits a binary.** C `free(p)` only marks the heap free; the **variable** `p` is still a number you can store through. rustc’s `Box` **owns** the `i32`. `drop(p)` **moves** that owner. After the move there is no pointer left (`E0382`). @@ -316,7 +294,7 @@ One hole, three spellings. Allocate an `int`, free it, write through the old poi ```c int *p = malloc(sizeof(int)); free(p); -*p = 42; /* gcc: -Wuse-after-free, then links */ +*p = 42; /* gcc: -Wuse-after-free, then a binary */ ``` @@ -340,24 +318,15 @@ fn main() { ``` ```text -$ rustc drop.rs error[E0382]: use of moved value: `p` - --> drop.rs:4:5 - | -2 | let mut p = Box::new(10); - | ----- move occurs because `p` has type `Box` -3 | drop(p); - | - value moved here -4 | *p = 42; - | ^^^^^^^ value used here after move ``` -**Why rustc rejects this**, not “because Rust is safer” as a slogan. `Box` **owns** the heap `i32`. `drop(p)` **moves** that owner into `drop`. After the move, `p` is gone. There is no pointer left to write. C `free(p)` only marks the heap free; the **variable** `p` is still a number you can store through. That is the language rule, not a smarter programmer. +That is the language rule, not a smarter programmer. Full `String` / `string_view` logs: [What I compiled](#what-i-compiled). -The longer `String` / `string_view` demos in [What I compiled](#what-i-compiled) are the same rule with more bytes. +Bounds are a different check: safe `v[100]` on a length-3 `Vec` is a **panic**, not C UB. `--release` does not turn it into UB. LLVM may omit the instruction only after a static proof that the index fits. ## Data races @@ -773,9 +742,9 @@ ERROR: AddressSanitizer: heap-buffer-overflow C++23 `string_view` after `delete` printed `secret` (exit 0). With ASan: `heap-use-after-free`, abort. -C++23 + ASan caught both **at run time**, if you pass the flag and **run the path**. rustc’s check on `&s` / `a[10]` is compile time with no extra flag. The variable-index panic is in every binary, debug or `-O`. +C++23 + ASan caught both **at run time**, if you pass the flag and **run the path**. rustc’s check on `&s` / `a[10]` is compile time with no extra flag. The variable-index panic is in every binary I built, including `-O`. `--release` is not an “unchecked indexing” mode. -[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) / `span::at` throw `out_of_range` — closer to Rust `v[i]`. The usual C++ spelling is still unchecked `v[i]` / `span[i]`. Rust’s usual spelling is the checked one; LLVM may drop the check when it proves the index. Unchecked Rust is `unsafe { *v.get_unchecked(i) }`. +[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) / `span::at` throw `out_of_range`. That is closer to Rust `v[i]`, and it is still **opt-in**. Default C++ `s[i]` is unchecked. Default Rust `v[i]` is checked. LLVM may omit a Rust check only after it proves the index fits. Boost became a lot of `std`; the rest of the kitchen sink is crates.io on the Rust side. Trusting a crate is the same problem as trusting Boost. Neither makes `span[i]` check bounds by default. @@ -801,6 +770,30 @@ pub fn as_static(s: &str) -> &'static str { “Our crate has no `unsafe`” is incomplete: `Cargo.lock` may pull crates that do. `cargo audit` finds **known** advisories; it does not prove libraries correct. +## Checklist + +:::tip Use this when you write `unsafe` or FFI +Four questions on the block, then `get_unchecked` only with a local proof. Full list below. +::: + +**`unsafe` block (write this in a comment above the block):** + +1. What invariant am I asserting? (non-null, aligned, `len` is the allocation, no alias with `&mut`, lifetime not `'static` unless it really is) +2. Who established it — this function, the caller, or C? +3. What would make it false on the next line? +4. Can Miri run this path in CI? + +**`get_unchecked`:** only after a local proof (`i < v.len()`, or an iterator that already walked the slice). If the proof is “I think the loop is fine,” keep `v[i]`. + +**FFI / C++ interop:** + +- Treat every `extern "C"` length and pointer as untrusted until you copy into a `Vec` / checked slice. +- Do not `from_raw_parts` on a C “success” that can be null + len 0 unless the C API documents that as empty. +- Prefer owning the allocation on one side. If C frees, Rust must not `Drop` the same bytes. +- On the C++ side: `unique_ptr` / `span::at` at the boundary; sanitizers on the C++ test binary, not only on the Rust crate. + +**Filesystem:** open once; `fstat` / operate on the fd; `O_NOFOLLOW` if the path must not be a symlink. + ## From borrow checking to LLVM Safe Rust is only as strong as rustc. If rustc **accepts an invalid program**, the language said no and the implementation said yes. LLVM then optimizes as if the type were true. That is a miscompile of the *language*, not a random backend crash. @@ -811,10 +804,14 @@ source → AST → HIR → type check → borrow check → MIR → LLVM IR → m Ownership is checked **before** LLVM. `&mut` is not “any C pointer”; rustc can lower it as `noalias`. If rustc **wrongly** emits `&'static`, LLVM may treat that pointer as forever. A hole in lifetime rules is permission for the optimizer. +Bounds checks in MIR are the same story: `--release` is not a switch that deletes them. LLVM may omit a check only when it can prove the index is in range. That is a proof of safety, not a weaker language. + The guarantee is not “the borrow checker is perfect.” It is: **the whole pipeline preserves the language’s safety rules.** rustc and LLVM are two later steps on that list. The [pipeline article](/docs/articles/rustc-pipeline-vs-cpp-compilation-pipeline) draws the C++ side. +The sections above assume rustc is correct. rustc is software too. Here is a real case where it is not. + ## Limits of rustc This is step 5 in [where the language guarantee ends](#where-the-language-guarantee-ends). The language model can be sound while **this rustc** accepts a program it should reject. LLVM then treats the lie as IR truth. @@ -823,7 +820,7 @@ Yusung Sim, Sukyoung Ryu (KAIST), Jaemin Hong (UNIST), [Rust's Type Checker Impl Three compiler-bug kinds: crash; reject good code; **accept bad code** (soundness). The last can hide UAF behind `cargo build` with no `unsafe`. Liu et al. (OOPSLA 2025) [[7]](#references) counted rustc bugs more broadly; ISSTA looks at type (3). Study set: abstract **23**, artifact **30** (plus 7 from Liu). -Hard cases: implied bounds, trait objects, associated types — not `Vec` indexing. [#25860](https://github.com/rust-lang/rust/issues/25860) (2015) [[2]](#references) is the long example. [Miri](https://github.com/rust-lang/miri) [[9]](#references) can catch some that blow up at run time. The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) lists #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). C and C++ also lack a full machine spec of “must reject.” Rust’s short sentence needs rustc to be right. +Hard cases: implied bounds, trait objects, associated types — not `Vec` indexing. The previous demos (`Vec`, `Box`, `a[i]`) assume rustc implements those rules. Below is a program with **no** `unsafe` that rustc 1.93.1 still accepted. ### The file I compiled (#25860) @@ -849,27 +846,7 @@ pub fn as_static(x: &T) -> &'static T { } ``` -I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. Normal `Vec` code is not this file. This is a rustc limit, not a `Vec` gotcha. - -## Checklist - -**`unsafe` block (write this in a comment above the block):** - -1. What invariant am I asserting? (non-null, aligned, `len` is the allocation, no alias with `&mut`, lifetime not `'static` unless it really is) -2. Who established it — this function, the caller, or C? -3. What would make it false on the next line? -4. Can Miri run this path in CI? - -**`get_unchecked`:** only after a local proof (`i < v.len()`, or an iterator that already walked the slice). If the proof is “I think the loop is fine,” keep `v[i]`. - -**FFI / C++ interop:** - -- Treat every `extern "C"` length and pointer as untrusted until you copy into a `Vec` / checked slice. -- Do not `from_raw_parts` on a C “success” that can be null + len 0 unless the C API documents that as empty. -- Prefer owning the allocation on one side. If C frees, Rust must not `Drop` the same bytes. -- On the C++ side: `unique_ptr` / `span::at` at the boundary; sanitizers on the C++ test binary, not only on the Rust crate. - -**Filesystem:** open once; `fstat` / operate on the fd; `O_NOFOLLOW` if the path must not be a symlink. +I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. Normal `Vec` code is not this file. This is a rustc limit, not a `Vec` gotcha. [Miri](https://github.com/rust-lang/miri) [[9]](#references) can catch some of these if they blow up at run time. The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) lists #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). ## What you pay for the checks From 9856f510d410d8c34f2a2603100d015ac48281bb Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:25:24 +0530 Subject: [PATCH 12/14] Last polish: panic vs throw, one guarantee-ends section, shorter run notes. Also bold the results table headers and note that the #25860 debug stop is rustc's internal check, not user code. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 51 +++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index c59003fa..e10fd98a 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -74,7 +74,7 @@ This piece is for people who already know what a use-after-free is and care wher People say **Rust is memory safe**. I wanted the compiler version of that sentence, not the slide. So I compiled the same bugs with rustc 1.93.1, gcc/g++ 13.3, and clang 18, then asked where the proof stops: language, rustc, LLVM, or the OS. -**The default index is the comparison.** In safe Rust, `v[i]` is the checked operation. In C++, `v[i]` / `span[i]` is the unchecked one; `v.at(i)` / `span::at` / GSL are extra. Safety-critical C++ often uses those extras. They remain a choice. `#![no_std]` still has the check in `core` unless you wrote `get_unchecked`. +**The default index is the comparison.** In safe Rust, `v[i]` is checked and **panics** on a miss. In C++, `v[i]` / `span[i]` is unchecked; `v.at(i)` / `span::at` **throw** — defined, but opt-in. `#![no_std]` still checks in `core` unless you wrote `get_unchecked`. C++ already has `unique_ptr`, `span`, sanitizers. The interesting difference is **what happens when the programmer forgets to use them.** @@ -94,8 +94,8 @@ The claim is real. It is also smaller than “Rust is memory safe.” **Results of the programs I compiled** (default flags unless noted: `-Wall -Wextra`, no sanitizer): -| Bug | C/C++ default | C/C++ + ASan/UBSan | Safe Rust | -|---|---|---|---| +| **Bug** | **C/C++ default** | **C/C++ + ASan/UBSan** | **Safe Rust** | +|:---|:---|:---|:---| | Use-after-free | Compiles (gcc may warn; clang often silent) | Runtime abort | Rejected (`E0515` / `E0382`) | | Constant OOB (`a[10]` on size 4) | Compiles (clang warns, still links) | Runtime report | Rejected (compile error) | | Runtime OOB (`a[i]`) | UB; my run smashed a neighbor `flag` | Often a message; default UBSan still continues | Panic, exit 101 | @@ -113,7 +113,7 @@ Evidence is below. Three different index stories, say them once: ## Table of Contents - [The short answer](#the-short-answer) -- [Three levels of Rust safety](#three-levels-of-rust-safety) +- [Where the language guarantee ends](#where-the-language-guarantee-ends) - [Ownership: static proof vs leftover pointer](#ownership-static-proof-vs-leftover-pointer) - [Data races](#data-races) - [What I compiled](#what-i-compiled) @@ -124,7 +124,6 @@ Evidence is below. Three different index stories, say them once: - [Limits of rustc](#limits-of-rustc) - [What you pay for the checks](#what-you-pay-for-the-checks) - [The claim I would actually make](#the-claim-i-would-actually-make) -- [How I ran this](#how-i-ran-this) - [References](#references) ## The short answer @@ -244,27 +243,25 @@ still running a[0]=0 flag=42 Sanitizers can name the same bugs. They are a flag you pass and a path you must run. Safe Rust’s reject/panic is the default. C does not mark `a[i] = 42` as `unsafe`. rustc bugs and FFI still sit outside that default; those sections come later. -## Three levels of Rust safety +## Where the language guarantee ends -The slide says “Rust.” That is several products glued together. +The slide says “Rust.” That is several products glued together. **Memory-safety as a language model** is not the same as **an implementation bug in rustc.** -**Level 1: safe Rust.** No `unsafe` in *your* function. rustc checks ownership, lifetimes, aliases, and (for `a[i]`) whether the index fits. +**Level 1: safe Rust.** No `unsafe` in *your* function. Ownership, borrows, bounds, data-race rules — if rustc is sound. -**Level 2: `unsafe`.** You told rustc to trust you. Callers still see a safe type. The proof is a human. +**Level 2: `unsafe`.** You assume the invariant. Callers still see a safe type. -**Level 3: FFI + the toolchain.** C libraries, ABI, file descriptors, `mmap`, rustc itself, LLVM. Language rules stop at `extern "C"`. They also stop if rustc is wrong. +**Level 3: FFI + the toolchain.** C lengths, ABI, `mmap`, libraries, rustc, LLVM, OS. Language rules stop at `extern "C"`. They also stop if rustc is wrong. -### Where the language guarantee ends +The numbered list is the same picture, one step finer: -This is the compiler article’s spine. **Memory-safety as a language model** is not the same as **an implementation bug in rustc.** - -1. **Safe Rust** — ownership, borrows, bounds, data-race rules (if rustc is sound). -2. **`unsafe`** — you assume the invariant. -3. **FFI** — rustc trusts C lengths, pointers, and ABI. -4. **Library implementation** — `std` and crates contain `unsafe`; `Cargo.lock` is in the TCB. -5. **rustc** — a soundness bug accepts code the language forbids. That is not “Rust lied”; the *implementation* did. [Limits of rustc](#limits-of-rustc) is that step: ISSTA 2026 [[5]](#references) and [#25860](https://github.com/rust-lang/rust/issues/25860) [[2]](#references). -6. **LLVM** — optimizes IR it was given (`noalias` on a lie is still “correct” LLVM). -7. **OS / hardware** — TOCTOU, `mmap`, permissions, cosmic rays. +1. **Safe Rust** +2. **`unsafe`** +3. **FFI** +4. **Library implementation** (`std` / crates; `Cargo.lock` is in the TCB) +5. **rustc** — [Limits of rustc](#limits-of-rustc): ISSTA 2026 [[5]](#references), [#25860](https://github.com/rust-lang/rust/issues/25860) [[2]](#references) +6. **LLVM** (`noalias` on a lie is still “correct” LLVM) +7. **OS / hardware** — TOCTOU, permissions The rest of the article fills that list with programs. @@ -354,7 +351,7 @@ Safe Rust wants `Arc>` or an `AtomicUsize`. Deadlock, starvation, and ## What I compiled -Same computer. **rustc 1.93.1** (`01f6ddf75`, 2026-02-11). **gcc/g++ 13.3.0**. **clang/clang++ 18.1.3**. Flags: `-Wall -Wextra` for C and C++. No AddressSanitizer unless I say so. Commands: +Same computer. **rustc 1.93.1** (`01f6ddf75`, 2026-02-11). **gcc/g++ 13.3.0**. **clang/clang++ 18.1.3**. Flags: `-Wall -Wextra` for C and C++. C++23 tests: `-std=c++23`. ASan: `-fsanitize=address`. ISSTA counts are from the public abstract and artifact. uutils notes are Canonical / Zellic / oss-security, not “every Rust CLI is clean.” ```bash gcc -Wall -Wextra uaf.c -o uaf_c # exit 0, warns -Wuse-after-free @@ -744,7 +741,7 @@ C++23 `string_view` after `delete` printed `secret` (exit 0). With ASan: `heap-u C++23 + ASan caught both **at run time**, if you pass the flag and **run the path**. rustc’s check on `&s` / `a[10]` is compile time with no extra flag. The variable-index panic is in every binary I built, including `-O`. `--release` is not an “unchecked indexing” mode. -[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) / `span::at` throw `out_of_range`. That is closer to Rust `v[i]`, and it is still **opt-in**. Default C++ `s[i]` is unchecked. Default Rust `v[i]` is checked. LLVM may omit a Rust check only after it proves the index fits. +[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) / `span::at` throw `std::out_of_range`. Rust `v[i]` **panics**. Both are defined stops. Rust’s stop is the default index; C++’s throw is opt-in. LLVM may omit a Rust check only after it proves the index fits. Boost became a lot of `std`; the rest of the kitchen sink is crates.io on the Rust side. Trusting a crate is the same problem as trusting Boost. Neither makes `span[i]` check bounds by default. @@ -846,7 +843,7 @@ pub fn as_static(x: &T) -> &'static T { } ``` -I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug build stopped inside a copy check. Release printed zeros. Normal `Vec` code is not this file. This is a rustc limit, not a `Vec` gotcha. [Miri](https://github.com/rust-lang/miri) [[9]](#references) can catch some of these if they blow up at run time. The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) lists #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). +I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug stopped in **rustc’s own internal copy/sanity check**, not a check I wrote. Release printed zeros. Allocator reuse is not specified; another machine or glibc may print garbage, crash, or look fine. Normal `Vec` code is not this file. This is a rustc limit, not a `Vec` gotcha. [Miri](https://github.com/rust-lang/miri) [[9]](#references) can catch some of these if they blow up at run time. The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) lists #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). ## What you pay for the checks @@ -894,13 +891,9 @@ I would not write: “Rust makes programs memory safe.” Too broad. I would wri The interesting difference between Rust and C++ is not whether safety tools exist. It is **what happens when the programmer forgets to use them.** -Rust did not delete the need for correctness. It moved a big piece of it into the type system. Memory safety is a floor, not the whole building. - -## How I ran this - -The small C/C++/Rust programs and #25860 were run on rustc 1.93.1, gcc/g++ 13.3, and clang/clang++ 18.1.3 on one machine. C++23 tests used `-std=c++23`; ASan used `-fsanitize=address`. #25860 is still open. ISSTA numbers come from the public abstract and artifact; I did not invent extra stats. uutils notes come from Canonical’s 2026 post, the Zellic PDF, and oss-security, not “every Rust CLI is clean.” Docs search and compile speed change every release. +Rust did not delete the need for correctness. It moved a big piece of it into the type system. Memory safety is a floor, not the whole building. C and Rust can live together. Checking more at compile time is a bet that computers got cheaper faster than human attention. -C and Rust can live together. People are still the expensive part. Checking more at compile time is a bet that computers got cheaper faster than human attention. I just wanted the extra words on the claim written down. +All runs used the versions and flags in [What I compiled](#what-i-compiled). #25860 is still open. ## References From c9cf7658b807d8d055ba49b984ae5ae085423899 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:27:12 +0530 Subject: [PATCH 13/14] Give the results table a real header row and a working logs link. Clarify that the #25860 debug stop is a compiler-internal assertion, not user code. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 73 ++++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index e10fd98a..8b7b3e6a 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -1,6 +1,6 @@ --- title: "Rust Claims, a Reality Check: Safety, Tools, and Systems Programming" -description: "What rustc actually proves, how that reaches LLVM, and where the proof stops. Compiled on rustc 1.93.1 vs gcc 13.3 and clang 18." +description: "Rust's safety guarantees are real, but they stop at unsafe, FFI, compiler bugs, and the OS. Compiled on rustc 1.93.1 vs gcc 13.3 and clang 18." keywords: - Rust memory safety - Rust safety guarantees @@ -57,7 +57,7 @@ import TabItem from '@theme/TabItem'; import Head from '@docusaurus/Head'; - + # Rust Claims, a Reality Check: What rustc Proves, and Where the Proof Stops @@ -94,15 +94,60 @@ The claim is real. It is also smaller than “Rust is memory safe.” **Results of the programs I compiled** (default flags unless noted: `-Wall -Wextra`, no sanitizer): -| **Bug** | **C/C++ default** | **C/C++ + ASan/UBSan** | **Safe Rust** | -|:---|:---|:---|:---| -| Use-after-free | Compiles (gcc may warn; clang often silent) | Runtime abort | Rejected (`E0515` / `E0382`) | -| Constant OOB (`a[10]` on size 4) | Compiles (clang warns, still links) | Runtime report | Rejected (compile error) | -| Runtime OOB (`a[i]`) | UB; my run smashed a neighbor `flag` | Often a message; default UBSan still continues | Panic, exit 101 | -| Data race (`n += 1` from two threads) | Compiles | Tool-dependent | Rejected (`E0499`) | -| TOCTOU (check path, swap, open) | Compiles; reads the swapped file | ASan silent | Compiles; same wrong file | -| Bad FFI length / `unsafe` lie | Possible | Not solved | Possible (Level 2–3) | -| Logic / ignored `Result` | Possible | Not solved | Possible | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BugC/C++ defaultC/C++ + ASan/UBSanSafe Rust
Use-after-freeCompiles (gcc may warn; clang often silent)Runtime abortRejected (E0515 / E0382)
Constant OOB (a[10] on size 4)Compiles (clang warns, still links)Runtime reportRejected (compile error)
Runtime OOB (a[i])UB; my run smashed a neighbor flagOften a message; default UBSan still continuesPanic, exit 101
Data race (n += 1 from two threads)CompilesTool-dependentRejected (E0499)
TOCTOU (check path, swap, open)Compiles; reads the swapped fileASan silentCompiles; same wrong file
Bad FFI length / unsafe liePossibleNot solvedPossible (Level 2–3)
Logic / ignored ResultPossibleNot solvedPossible
Evidence is below. Three different index stories, say them once: @@ -368,9 +413,9 @@ clang -O0 -Wall -Wextra slice_i.c -o slice_i_clang ./slice_i_clang 4 # still running, flag smashed ``` -The UAF / constant-OOB / runtime-index sources match [the short answer](#the-short-answer). Full listings: +The UAF / constant-OOB / runtime-index sources match [the short answer](#the-short-answer). [Full sources and compiler logs for tests 1–3](#full-compiler-logs): -
+
Full sources and compiler logs for tests 1–3 ### 1. Use memory after free @@ -843,7 +888,7 @@ pub fn as_static(x: &T) -> &'static T { } ``` -I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug stopped in **rustc’s own internal copy/sanity check**, not a check I wrote. Release printed zeros. Allocator reuse is not specified; another machine or glibc may print garbage, crash, or look fine. Normal `Vec` code is not this file. This is a rustc limit, not a `Vec` gotcha. [Miri](https://github.com/rust-lang/miri) [[9]](#references) can catch some of these if they blow up at run time. The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) lists #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). +I compiled this with **rustc 1.93.1**. It accepted it. I dropped a `String`, allocated something the same size, then read the “forever” string. Debug hit a **compiler-internal assertion** (not a check in my source). Release built and ran, printing zeros. Allocator reuse is not specified; another machine or glibc may print garbage, crash, or look fine. Normal `Vec` code is not this file. This is a rustc limit, not a `Vec` gotcha. [Miri](https://github.com/rust-lang/miri) [[9]](#references) can catch some of these if they blow up at run time. The [compiler guide](https://rustc-dev-guide.rust-lang.org/traits/implied-bounds.html) lists #25860, [#84591](https://github.com/rust-lang/rust/issues/84591), [#100051](https://github.com/rust-lang/rust/issues/100051). ## What you pay for the checks From 57493af643af642b7addad1f78b5a1d77b6816a0 Mon Sep 17 00:00:00 2001 From: compilersutra Date: Sun, 16 Aug 2026 10:32:16 +0530 Subject: [PATCH 14/14] Correct span::at: it is C++26, not C++23. C++23 checked indexing in the article is vector::at or GSL; the C++23 span[i] experiment is unchanged. Co-authored-by: Cursor --- docs/articles/rust-claims-a-reality-check.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/articles/rust-claims-a-reality-check.md b/docs/articles/rust-claims-a-reality-check.md index 8b7b3e6a..fc39c912 100644 --- a/docs/articles/rust-claims-a-reality-check.md +++ b/docs/articles/rust-claims-a-reality-check.md @@ -1,6 +1,6 @@ --- title: "Rust Claims, a Reality Check: Safety, Tools, and Systems Programming" -description: "Rust's safety guarantees are real, but they stop at unsafe, FFI, compiler bugs, and the OS. Compiled on rustc 1.93.1 vs gcc 13.3 and clang 18." +description: "Rust's safety guarantees are real, but they stop at unsafe, FFI, compiler bugs, and the OS. Compiler logs and a practical checklist." keywords: - Rust memory safety - Rust safety guarantees @@ -57,13 +57,13 @@ import TabItem from '@theme/TabItem'; import Head from '@docusaurus/Head'; - + # Rust Claims, a Reality Check: What rustc Proves, and Where the Proof Stops :::tip In short -Safe Rust rejects use-after-free, constant out-of-bounds, and data races at compile time. Runtime out-of-bounds **panics by default** (`debug` and `--release`), unless LLVM can *prove* the index is in range — that elision does not weaken the rule. C and C++ need sanitizers or wrappers (`v.at(i)`, `span::at`) for the same class of bug; those are opt-in. `unsafe`, FFI, rustc/LLVM bugs, and logic errors sit outside that bubble. +Safe Rust rejects use-after-free, constant out-of-bounds, and data races at compile time. Runtime out-of-bounds **panics by default** (`debug` and `--release`), unless LLVM can *prove* the index is in range — that elision does not weaken the rule. C and C++ need sanitizers or wrappers (`v.at(i)`; C++26 `span::at`; GSL) for the same class of bug; those are opt-in. `unsafe`, FFI, rustc/LLVM bugs, and logic errors sit outside that bubble. ::: :::note @@ -74,7 +74,7 @@ This piece is for people who already know what a use-after-free is and care wher People say **Rust is memory safe**. I wanted the compiler version of that sentence, not the slide. So I compiled the same bugs with rustc 1.93.1, gcc/g++ 13.3, and clang 18, then asked where the proof stops: language, rustc, LLVM, or the OS. -**The default index is the comparison.** In safe Rust, `v[i]` is checked and **panics** on a miss. In C++, `v[i]` / `span[i]` is unchecked; `v.at(i)` / `span::at` **throw** — defined, but opt-in. `#![no_std]` still checks in `core` unless you wrote `get_unchecked`. +**The default index is the comparison.** In safe Rust, `v[i]` is checked and **panics** on a miss. In C++, `v[i]` / `span[i]` is unchecked. `v.at(i)` **throws** (C++98 onward for `vector`). [`span::at`](https://en.cppreference.com/w/cpp/container/span/at) is **C++26**, not C++23. Until then, checked span access is GSL or a project wrapper. `#![no_std]` still checks in `core` unless you wrote `get_unchecked`. C++ already has `unique_ptr`, `span`, sanitizers. The interesting difference is **what happens when the programmer forgets to use them.** @@ -769,7 +769,7 @@ Panic / OOM are still bugs. They are not [`strcpy`](https://en.cppreference.com/ Default C++ still lets you `new`/`delete` and `v[i]`. C++ also has tools. I re-ran the bugs with **C++23**, `std::span`, `std::vector`, and [ASan](https://github.com/google/sanitizers/wiki/AddressSanitizer) (`g++` 13.3 / `clang++` 18.1.3, `-std=c++23`). -`operator[]` on `std::span` does **not** check `i`. `span::at` does (throws), same split as `vector`. Safety-critical code often uses `at()`, GSL `span`, or a project wrapper. Default `s[i]` on a length-4 vector still wrote past the end in my C++23 build: +`operator[]` on `std::span` does **not** check `i`. A bounds-checked [`span::at`](https://en.cppreference.com/w/cpp/container/span/at) is **C++26** (P2821). My tests used `-std=c++23`, so that member was not in the language I compiled. In C++23, safety-critical code uses [`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at), [GSL `span`](https://github.com/microsoft/GSL), or a project wrapper. Default `s[i]` on a length-4 vector still wrote past the end in my C++23 build: ```text $ g++ -std=c++23 -O0 -Wall -Wextra cxx23_span.cpp -o cxx23_span @@ -786,9 +786,9 @@ C++23 `string_view` after `delete` printed `secret` (exit 0). With ASan: `heap-u C++23 + ASan caught both **at run time**, if you pass the flag and **run the path**. rustc’s check on `&s` / `a[10]` is compile time with no extra flag. The variable-index panic is in every binary I built, including `-O`. `--release` is not an “unchecked indexing” mode. -[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) / `span::at` throw `std::out_of_range`. Rust `v[i]` **panics**. Both are defined stops. Rust’s stop is the default index; C++’s throw is opt-in. LLVM may omit a Rust check only after it proves the index fits. +[`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at) throws `std::out_of_range`. Rust `v[i]` **panics**. Both are defined stops. Rust’s stop is the default index; C++’s throw is opt-in. C++26 `span::at` matches `vector::at`. LLVM may omit a Rust check only after it proves the index fits. -Boost became a lot of `std`; the rest of the kitchen sink is crates.io on the Rust side. Trusting a crate is the same problem as trusting Boost. Neither makes `span[i]` check bounds by default. +Many Boost features moved into `std`. The rest of that kitchen sink is crates.io on the Rust side. Trusting a crate is the same problem as trusting a Boost module. Neither makes C++23 `span[i]` check bounds by default. ## `unsafe` is a proof boundary @@ -832,7 +832,7 @@ Four questions on the block, then `get_unchecked` only with a local proof. Full - Treat every `extern "C"` length and pointer as untrusted until you copy into a `Vec` / checked slice. - Do not `from_raw_parts` on a C “success” that can be null + len 0 unless the C API documents that as empty. - Prefer owning the allocation on one side. If C frees, Rust must not `Drop` the same bytes. -- On the C++ side: `unique_ptr` / `span::at` at the boundary; sanitizers on the C++ test binary, not only on the Rust crate. +- On the C++ side: `unique_ptr` / `vector::at` (C++26: `span::at`) at the boundary; sanitizers on the C++ test binary, not only on the Rust crate. **Filesystem:** open once; `fstat` / operate on the fd; `O_NOFOLLOW` if the path must not be a symlink. @@ -957,7 +957,7 @@ Claims in the article point here: rustc soundness and #25860 (2–9); uutils / G 11. Canonical, [An update on rust-coreutils](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773); [Zellic audit PDF](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf); [oss-security CVE list](https://www.openwall.com/lists/oss-security/2026/05/02/2). 12. [CVE-2026-35344](https://github.com/advisories/GHSA-wh8p-h9hw-x2mc). [CVE-2026-56392](https://osv.dev/vulnerability/CVE-2026-56392); [CERT Polska on GNU coreutils](https://cert.pl/en/posts/2026/07/CVE-2026-56391/). 13. [TOCTOU](https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use); Rust [`std::fs`](https://doc.rust-lang.org/std/fs/); [FFI](https://doc.rust-lang.org/nomicon/ffi.html); [panic](https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html); [`strcpy`](https://en.cppreference.com/w/c/string/byte/strcpy); [undefined behavior (C)](https://en.cppreference.com/w/c/language/behavior). -14. [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer); [UBSan](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html); [`std::span`](https://en.cppreference.com/w/cpp/container/span); [`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at); [Boost](https://www.boost.org/); [crates.io](https://crates.io/). +14. [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer); [UBSan](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html); [`std::span`](https://en.cppreference.com/w/cpp/container/span); [`span::at` (C++26)](https://en.cppreference.com/w/cpp/container/span/at); [`vector::at`](https://en.cppreference.com/w/cpp/container/vector/at); [Boost](https://www.boost.org/); [crates.io](https://crates.io/). 15. rustdoc [Search](https://doc.rust-lang.org/nightly/rustdoc/read-documentation/search.html); [#19190](https://github.com/rust-lang/rust/issues/19190). 16. [Parallel Front End (2026)](https://rust-lang.github.io/rust-project-goals/2026/parallel-front-end.html); Nethercote, [July 2026](https://nnethercote.github.io/2026/07/31/how-to-speed-up-the-rust-compiler-in-july-2026.html). 17. [cargo-dist](https://github.com/axodotdev/cargo-dist), [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), [rust-streaming](https://github.com/emk/rust-streaming).