Everything in C is undefined behavior (and your code is probably already wrong)

5 min read 1 source explainer
├── "C's undefined behavior is so pervasive that even expert code routinely violates the standard"
│  └── Thomas Habets (blog.habets.se) → read

Habets argues that the C abstract machine is so narrow that writing non-trivial UB-free code requires near-superhuman discipline. He demonstrates this by showing that glibc, the Linux kernel, and CPython — code written by the people who define 'good C' — all ship undefined behavior in production releases.

├── "The C standard itself is broken and grants compilers too much license"
│  └── @Hacker News commenters (camp 1) (Hacker News) → view

This camp argues the standard's permission for compilers to assume UB never happens is the root problem. They point to compiler optimizations that delete null checks after pointer dereferences as evidence that the spec, not the programmers, is at fault.

├── "The post overstates the case by ignoring implementation-defined behavior and de-facto compiler guarantees"
│  └── @Hacker News commenters (camp 2) (Hacker News) → view

This camp pushes back that real-world C compilers offer de-facto guarantees beyond the strict standard, and that implementation-defined behavior fills many of the gaps Habets flags. They argue the alarm is overblown because no real toolchain treats every theoretical UB as license to miscompile.

├── "The pragmatic answer is to switch to Rust or another memory-safe language"
│  └── @Hacker News commenters (camp 3) (Hacker News) → view

A third faction in the comments quietly suggests that if writing defined C is this hard even for experts, the rational move is to adopt a language like Rust where the type system rules out entire classes of UB by construction. They treat the post as further evidence that C's safety model is unsalvageable.

└── "Modern tooling has largely closed the gap between standard-compliant and provably-correct C"
  └── top10.dev editorial (top10.dev) → read below

The editorial notes that UBSan, ASan, MSan, TSan, KCSAN, and modern Clang/GCC static analyzers now catch most of the UB Habets complains about — if developers actually run them. The real gap today is not detection capability but discipline in adopting the available sanitizers.

What happened

A blog post titled *Everything in C is undefined behavior* hit the front page of Hacker News with 450 points and a sprawling comment thread. The author, Thomas Habets, walks through a series of innocuous-looking C snippets — the kind that show up in production code, textbooks, and the standard library itself — and shows that each one invokes undefined behavior under the C standard. Signed integer overflow. Reading an uninitialized variable. Pointer arithmetic that walks one element past the end of an array. Comparing pointers from different allocations. Calling `memcpy` with a NULL source and a zero length. The list keeps going.

The thesis is uncomfortable: it's not that careless C programmers write UB — it's that the C abstract machine is so narrow that writing a non-trivial program without UB requires near-superhuman discipline, and most code in the wild, including code you trust, doesn't clear that bar. The post leans on examples from glibc, the Linux kernel, and CPython, demonstrating that even the people who define what "good C" looks like ship undefined behavior in shipping releases. The comment section split predictably: one camp arguing the standard is broken, another arguing the post overstates the case by ignoring implementation-defined behavior and the de-facto guarantees compilers offer, and a third quietly suggesting Rust.

None of this is new. What's new is how thoroughly tooling has caught up. UBSan, ASan, MSan, TSan, KCSAN, and the static analyzers in modern Clang and GCC now catch most of what the post complains about — if you run them. The gap between "my code compiles" and "my code is defined under the standard" is wider than ever, but the gap between "my code is defined" and "I can prove it" has narrowed dramatically.

Why it matters

The core problem is that the C standard grants the compiler license to assume UB never happens. That assumption isn't theoretical. When GCC sees `if (p->x) { ... } use(p);` it can delete the NULL check, because dereferencing `p` earlier means `p` couldn't have been NULL — and if it was, the program was already wrong, so the optimizer owes you nothing. This is the famous CVE-2009-1897 pattern in the Linux kernel, where a NULL check was optimized away and turned into a local privilege escalation. The compiler did exactly what the standard allows. The kernel developers learned to compile with `-fno-delete-null-pointer-checks`, which is itself a tacit admission that the standard's semantics are unusable for systems software.

The post's examples are damning because they're boring. `int x = INT_MAX + 1;` is UB. `int *p; if (cond) p = &y; *p;` is UB if `cond` was false. Calling `qsort` with a comparator that doesn't establish a total order is UB. Even `memcpy(dst, NULL, 0)` was UB until C23 finally fixed it — and there's still production code, written before 2024, that triggers the old rule. If you're maintaining a C codebase older than three years, you almost certainly have UB in it; the only question is whether your compiler has decided to weaponize that knowledge yet.

Where the post overstates: not all UB is equally dangerous. Signed overflow on x86 generally does the obvious two's-complement wrap at -O0, and your code may work for a decade before an upgrade to GCC 14 introduces an optimization that exploits the UB and silently breaks it. That's the asymmetry — UB is a latent bug that activates when the compiler gets smarter. Rust's argument has always been that the language should make the safe path the easy path; the C community's counter-argument, that thirty years of code can't simply be rewritten, is correct but increasingly beside the point. The question isn't whether to rewrite C, it's whether you can defend the C you already have.

The comments contained one particularly sharp observation from a longtime kernel contributor: most of the UB-related bugs that ship to users aren't found by careful reading of the standard. They're found by running fuzzers and sanitizers in CI and treating the output as a build error. The standard is a document; the sanitizers are an executable contract.

What this means for your stack

If you ship C or C++, three things should happen this quarter. First, add `-fsanitize=undefined,address` to your CI test runs and make any sanitizer report fail the build — not warn, fail. UBSan in particular is cheap, catches signed overflow, alignment violations, and array bounds at runtime, and finds bugs that have been in your codebase for years. The performance cost is real (often 2-3x slower) but you only pay it in CI, not in production.

Second, audit your compiler flags. `-fwrapv` makes signed overflow defined as two's-complement wrap, which is what most C programmers think the language already does. `-fno-strict-aliasing` disables the type-based aliasing optimizations that turn pointer-punning UB into miscompiles. `-fno-delete-null-pointer-checks` keeps your NULL guards intact. These flags cost you some optimization headroom, but they convert latent UB into defined behavior — which is usually the right trade for systems code where correctness beats microbenchmarks. The Linux kernel uses all three.

Third, if you're writing new code at the systems layer and have a choice, the case for Rust, Zig, or even modern C++ with `std::span` and `std::expected` is stronger than it was even two years ago. The argument isn't that C is dead — it's that writing correct C now requires the discipline of writing Rust, plus the tooling of writing Rust, minus the guarantees of writing Rust. If you're already paying the discipline tax, the language is the cheapest part of the migration.

Looking ahead

C isn't going anywhere — it's the lingua franca of operating systems, embedded firmware, and the bottom of every language runtime. But the assumption that "experienced C programmers don't hit UB" has been quietly refuted by twenty years of sanitizer output. The practical path forward isn't to argue with the standard; it's to instrument your builds so the compiler can't surprise you. The post that started this conversation will have done its job if a few thousand more codebases turn on UBSan tomorrow.

Hacker News 450 pts 594 comments

Everything in C is undefined behavior

→ read on Hacker News

// share this

// get daily digest

Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.