NEWS

C2y removes 45 of the roughly 100 undefined behaviors in the C standard

In a talk at Kernel Recipes 2026, researcher Martin Uecker showed how the C committee is reducing the language's undefined behaviors without giving up the compatibility that still underpins kernels and embedded systems today.

Who is Martin Uecker and why he still defends C in 2026

Martin Uecker is not the usual speaker profile at kernel conferences: he is a professor of biomedical engineering and develops free software to control magnetic resonance imaging devices. But he has been a Linux user for decades and sits on the committee that defines the future of the C language. At the 2026 edition of Kernel Recipes, he presented an overview of undefined behavior in C and how far the language can go toward memory safety, according to a report by LWN.net published on September 28, 2026.

The question he answered right at the start: why use C in 2026? According to him, C remains portable, stable over the long term, compiles fast, and produces fast binaries. The central argument is the predictability of the code.

What you see is what you get.

Martin Uecker, biomedical engineering professor and Linux developer

What the standard calls undefined behavior

C89 had to deal with extremely heterogeneous hardware: machines with sign-and-magnitude or one's complement integer representations, segmented memory, exotic pointers, and even nine-bit bytes (some Honeywell machines). To allow portable code in that scenario, the standard defines the language in terms of an abstract machine: the compiler only needs to guarantee that the observable behavior of the program (access to volatile variables, for example) matches what the abstract machine would produce. Everything that is not observable is free for the compiler to optimize however it wants.

Undefined behavior appears when the program does something the standard simply does not define. In these cases, C89 says that the implementation "imposes no requirements." This exists for practical reasons: to allow extensions, to handle hardware security mechanisms, and, above all, to make room for aggressive optimizations.

Nasal demons: when the compiler does whatever it wants

The problem, according to Uecker, is that the standard allows the compiler to do absolutely anything in the face of undefined behavior, which the community nicknamed "nasal demons": if there is UB anywhere in the program, it stops having the expected semantics. An example from the standard itself: zeroing out an entire struct with memset() and then writing to only some of its fields. What happens if someone reads the padding bytes? A 2015 survey showed there was no consensus about this among compiler implementers.

Another case shown in the talk:

c
extern int x;

int f(int a, int b)
{
 x = b ? 42 : 43;
 return a/b;
}

If b is zero, a/b is division by zero, that is, undefined behavior. Some compilers understand that, since this path has no defined semantics, they can simply eliminate the test and execute x = 42 directly. But replace the call with a function:

c
extern void g(int x);

g(b ? 42 : 43);

If g() calls exit() when x is zero, the division will never happen, so the program has no UB at all. Compilers that eliminated the test in this case simply had a bug, rather than exercising a legitimate freedom granted by the standard.

A third case involves a volatile variable: could the compiler move a division ahead of an assignment to that variable, since, if there is no defined semantics on the division-by-zero path, there would be no change in observable behavior? C23 closed that loophole with a rule dubbed "no time travel," prohibiting that kind of reordering. In C++, the same protection depends on manually inserting a call to std::observable_checkpoint().

C2y has already cut almost half of the gray areas

The C committee today maintains three study groups dedicated specifically to the memory object model, memory safety, and undefined behavior. According to Uecker, the current standard has around 100 catalogued instances of undefined behavior, and the ongoing C2y draft has already removed 45 of them.

This is a concrete advance, not just a promise: C23 had already eliminated old K&R-style function definitions, support for sign-and-magnitude and one's complement machines, and trigraphs, in addition to adding bit-precise integer types and overflow-checking arithmetic operations. C2y, still in draft, is expected to bring ranges in case labels, named for loops, and the _Countof() macro to get the size of arrays, in what Uecker called a new round of "demon removal."

The tools that catch what the standard still doesn't solve

While the standard evolves slowly, the set of tools for finding undefined behavior has grown considerably: compiler warnings, static analyzers, sanitizers, LLM-based tools, and formal verification. GCC, for example, already emits warnings for a number of buffer overflow situations, and sanitizers can detect UB at runtime, including in trapping mode, which is useful both for debugging and for hardening in production.

Memory safety: three problems, uneven progress

Uecker split memory safety in C into three distinct fronts:

  • Type safety: C already has a reasonable type system; untagged unions can create type confusion, but additional annotations let the compiler enforce this, and new diagnostics already catch unsafe casts from void.
  • Spatial safety (bounds checking): partially solved. Compilers already perform array bounds checking in many cases; the counted_by attribute allows checking flexible array members when the code is adjusted for it.
  • Temporal safety (avoiding use-after-free and the like): this is the hardest part, and here Rust has a clear advantage, according to Uecker. Still, architectures like CHERI help reinforce this in hardware, and the Fil-C tool already finds a good share of temporal safety bugs.

According to him, achieving full memory safety in C will require either expensive runtime checking or formal verification; in the short term, the most robust result combines a restricted language with formal verification tools.

What this changes for those maintaining C in production

C still runs under the hood of the Linux kernel, embedded systems, firmware, and much of the critical infrastructure that runs banks, telecom operators, and industry in Brazil, exactly the kind of code where a misunderstood undefined behavior turns into a silent security flaw. The news here isn't that C became a memory-safe language (it didn't, and Uecker was clear about that), but that the path toward reducing UB is measurable: from ~100 catalogued cases down to 55 in the C2y draft, plus the formal closing of the "time travel" case in C23.

In practice, for those maintaining C code today, this means three moves that can already be adopted without waiting for C2y to become a final standard: enable the latest overflow and use-after-free warnings in GCC and Clang, run sanitizers in trapping mode in CI, and, when dealing with flexible-size arrays, annotate them with counted_by instead of relying on code convention.

For new code where temporal memory safety is critical, it's worth considering interoperating with Rust or keeping an eye on the progress of architectures like CHERI, rather than waiting for C alone to solve this. The video and slides from Uecker's talk are available through LWN.net's coverage for anyone who wants the full technical details.

Translated from the Brazilian Portuguese original · Read the original