We're bad at marketingAs a prof of biomedical engineering, Martin Uecker perchance does not fit the overview of a representative presenter at Kernel Recipes. He is, however, a longtime Linux user, and plant on liberated application for controlling magnetic resonance imaging (MRI) scanners. He was at the gathering to talk concerning the C programming language, the particular issue of undefined behavior in C, and whether it can eventually be made into a memory-safe language.We can acknowledge it, promotion is not our powerful suit. Our power is writing the benevolent of articles that developers, administrators, and free-software supporters depend on to cognize what is going on in the Linux world. Please subscribe today to assistance us keep doing that, and so we don’t have to get fine at marketing.
Why annoy alongside C in 2026? It is, he said, motionless a awesome language. C is portable, stable complete the lengthy term, offers accelerated compilation, and the resulting binary code is fast. "What you see is what you get"; it is uncomplicated to appearance at C code and have several idea of what the device will actually do. There are a lot of tools for operating alongside the language, and C gets out of the way whenever necessary.
C does have a lengthy history, and that affects the tongue as we see it today, he said. The C89 norm had to oversee alongside a broad assortment of hardware, including machines alongside signed-magnitude or one's-complement integer representations, segmented memory, exotic pointer representations, and amazing sizes for types. Some Honeywell machines, for example, had nine-bit bytes. That greatly complex the project of penning a standard that would allow the penning of portable code.
The method that was taken was to define the semantics of the tongue in terms of an theoretical machine. All operations are to be executed as if they had run on that theoretical machine, which may not exactly equivalent the actual hardware. The observable behavior of the program must be what the abstract machine would have done. The "observable" part matters: admission to volatile variables, being defined as observable, must happen exactly according to the theoretical machine; everything alternatively fair has to produce the identical eventual result.
The norm gives a lot of liberty to compiler implementers; lone the observable behavior have to be preserved. There are many aspects of that behavior that are either undefined or implementation-defined. These are not observable behavior, and thus do not constrain what compiler implementers can do. There are, of course, another details that can constrain compiler developers anywhere the C norm does not; these contain ABI requirements, standards akin POSIX, or the need for backward compatibility.
Undefined behavior comes concerning whenever a program does item that is either not portable or not defined by the norm at all. In specified cases, the C89 standard states that it "imposes no requirements" on the implementation. Undefined behavior exists for a figure of reasons. It allows implementations to assistance extensions, manage interactions alongside hardware-based safety mechanisms, and execute aggressive optimization, all during allowing difficult-to-detect errors to be ignored. It explicitly gives the compiler the correct to disregard entire classes of hard-to-detect errors.
Nasal demons
The problem, Uecker said, is that the norm allows a compiler to do anything in reply to undefined behavior, up to the item of invoking nasal demons. If a program contains any undefined behavior at all, according to compiler writers, afterward it has no expected semantics. The C++23 standard goes additional to explicitly province that the norm imposes no requirements for these programs. That has led to general disagreements between developers concerning what can be expected from the language.
For example, if you zero an complete construction (perhaps alongside a call to memset()), then compose to particular fields, what volition happen if you peruse from any padding bytes in that structure? Might they merge security-relevant data? A 2015 survey showed that there was no agreement on what should happen in that case. Or regard this uncomplicated code:
extern int x; int f(int a, int b) { x = b ? 42 : 43; return a/b; } If b is zero, afterward the come back declaration is a division by zero, which is undefined behavior. In this case, is the compiler entitled to omit the test entirely and fair execute x = 42? After all, the b = 0 case has no expected semantics, and can thus be ignored. There are compilers that volition do exactly that. In the undefined-behavior case, the shop to x is not observable behavior. But now consider this case:
extern void g(int x); int f(int a, int b) { g(b ? 42 : 43); return a/b; } This power appear to be the identical situation, alongside the compiler being entitled to eliminate the test and fair continue 42 to g(), and several compilers have treated that way — but that compiler behavior was a bug. Imagine a definition of g() that calls exit() if b is zero. In that case, the division volition never happen and the program's behavior is not undefined. So eliding the test and merely passing 42 to g() is incorrect.
One additional engaging case:
volatile int x; int foo(int a, int b, bool store_to_x) { if (! store_to_x) come back a/b; x = b; return a/b; } The inquiry current is: can the compiler hoist the final division operation above project to x? If there are no semantics connected with the b = 0 case, afterward there is no alter in observable behavior. This, too, is item compilers have done, but the C23 norm added a "no period travel" stipulation to disallow it. In C++, instead, hoisting must be explicitly prevented by inserting a call to std::observable_checkpoint().
Time-travel bugs should eventually go away, but there are a lot of other situations where, equal if the norm is clear, compiler writers often disagree. These contain study of uninitialized variables (which is almost continually defined), and equality comparisons of pointers, which is continually defined, but is additionally miscompiled by the two Clang and GCC.
Fighting undefined behavior
To try to location all of these problems and more, the C commission runs three study groups focused specifically on the recollection entity model, memory safety, and undefined behavior. There are currently concerning 100 instances of undefined behavior in the C standard, but the in-progress C2y outline has removed 45 of them. The circumstance is certainly getting better.
There is an increasingly affluent set of tools aimed at finding issues: compiler warnings, fixed analyzers, sanitizers, LLM-based tools, formal verification, and more. The figure of situations anywhere a compiler will emit a alert anywhere imaginable undefined behavior is detected is growing; recent examples contain improved warnings for entire figure overflows and potential use-after-free situations. Static analyzers are accessible as standalone tools, but are additionally increasingly being built into the compilers themselves; GCC can now notify concerning a figure of possible buffer-overflow situations, for example. Sanitizers activity by inserting run-time checks; they can catch a lot of undefined behavior and, in trapping mode, be used for hardening as well.
Memory safety has never been one of C's powerful points, but Uecker wanted to make the item that it can be improved. That issue breaks downward into three sub-problems: category safety, spatial recollection safety, and temporal memory safety.
C, he said, has a powerful category system, and the remaining problems are fixable. Tagless unions, for example, can create category confusion, but the compiler can enforce types alongside several additional annotations. New diagnostics can capture unsafe casts from void. Type checking across translation units is traditionally not a huge issue in C, since header records are used to justify accordant types, but the circumstance could be improved alongside a link-time checker.
Spatial recollection safety — limits checking — is a partially solved problem; the compilers can execute array-bounds checking in many situations now. In some cases, several code changes are needed to completely advantage from this checking. Use of the counted_by attribute can allow checking for elastic gathering members, for example.
Temporal recollection safety — avoiding use-after-free bugs and the akin — is harder, Uecker said, and Rust certainly has an advantage there. Still, better temporal memory-safety implementation is possible. Architectures like CHERI can assistance current is well. Fil-C can discover a lot of temporal-safety bugs.
Can all of these tools and tongue changes get us to complete recollection safety? Completely solving the issue volition necessitate either costly run-time checking or ceremonial verification, he said. In the near future, the most complete results volition be had alongside the blend of a restricted language and ceremonial verification tools.
Overall, he concluded, C is motionless a living tongue and is motionless improving. The C23 norm removed a figure of problematic features, including old-style (K&R) function definitions, assistance for sign-magnitude and one's-complement machines, and trigraphs. It added bit-precise integer types, checked entire figure operations, and more. C2y volition go further, adding case ranges, named for loops, the _Countof() macro to determine gathering lengths, and a lot of "demon removal". It volition not achieve complete recollection safety for C, but that is an eventual possibility, and will rotate into additional applicable complete time. He ended by encouraging interested people to act in the operating groups.
The video and slides from this talk are available.
[Thanks to the Linux Foundation, LWN's journey sponsor, for supporting my travel for this event.]