{"article":{"slug":"reducing-undefined-behavior-in-the-c-language","title":"Reducing undefined behavior in the C language","subtitle":null,"summary":"LWN's Jonathan Corbet reports on Martin Uecker's Kernel Recipes 2026 talk about undefined behavior in C: why it exists, how compilers exploit it, how the C committee's study groups and the C2y draft are removing cases, and how tooling is moving C toward better memory safety.","content_type":"article","language":"en","canonical_url":"https://lwn.net/Articles/1095811/","author":{"name":"Jonathan Corbet","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"LWN.net","url":"https://lwn.net/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1508,"reading_minutes":7,"published_at":"2026-09-28T00:00:00.000Z","added_at":"2026-10-09T08:16:53.307Z","updated_at":"2026-10-09T08:16:53.307Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":false},"profile_url":"https://listedarticles.com/articles/reducing-undefined-behavior-in-the-c-language","markdown_url":"https://listedarticles.com/articles/reducing-undefined-behavior-in-the-c-language.md","example":false,"citation":"Jonathan Corbet, LWN.net. \"Reducing undefined behavior in the C language.\" 28 Sept 2026. https://lwn.net/Articles/1095811/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://lwn.net/Articles/1095811/"},"body_markdown":"# Reducing undefined behavior in the C language\n\nBy Jonathan Corbet, September 28, 2026\n\nAs a professor of biomedical engineering, Martin Uecker perhaps does not fit the profile of a typical presenter at [Kernel Recipes](https://kernel-recipes.org/en/2026/). He is, however, a longtime Linux user, and works on free software for controlling magnetic resonance imaging (MRI) scanners. He was at the conference to talk about the C programming language, the specific problem of undefined behavior in C, and whether it can eventually be made into a memory-safe language.\n\nWhy bother with C in 2026?  It is, he said, still a great language.  C is\nportable, stable over the long term, offers fast compilation, and the\nresulting binary code is fast.  \"What you see is what you get\"; it\nis easy to look at C code and have some idea of what the computer will\nactually do.  There are a lot of tools for working with the language, and C\ngets out of the way when necessary.\n\nC does have a long history, and that affects the language as we see it today, he said. The C89 standard had to cope with a wide variety of hardware, including machines with signed-magnitude or one's-complement integer representations, segmented memory, exotic pointer representations, and surprising sizes for types. Some Honeywell machines, for example, had nine-bit bytes. That greatly complicated the task of writing a standard that would enable the writing of portable code.\n\nThe approach that was taken was to define the semantics of the language in\nterms of an abstract machine.  All operations are to be executed as if they\nhad run on that abstract machine, which may not exactly match the actual\nhardware.  The observable behavior of the program must be what the abstract\nmachine would have done.  The \"observable\" part matters: access to\n`volatile` variables, being defined as observable, must happen\nexactly according to the abstract machine; everything else just has to\nproduce the same eventual result.\n\nThe standard gives a lot of freedom to compiler implementers; only the\nobservable behavior has to be preserved.  There are many aspects of that\nbehavior that are either undefined or implementation-defined.  These are\nnot observable behavior, and thus do not constrain what compiler\nimplementers can do.  There are, of course, other specifications that\n*can* constrain compiler developers where the C standard does not;\nthese include ABI requirements, standards like POSIX, or the need for\nbackward compatibility.\n\nUndefined behavior comes about when a program does something that is either\nnot portable or not defined by the standard at all.  In such cases, the C89\nstandard states that it \"imposes no requirements\" on the\nimplementation.  Undefined behavior exists for a number of reasons.\nIt allows implementations to support extensions, manage\ninteractions with hardware-based safety mechanisms, and perform aggressive\noptimization, all while allowing difficult-to-detect errors to be ignored.\nIt explicitly gives the compiler the right to ignore whole classes of\nhard-to-detect errors.\n\n\n#### Nasal demons\n\nThe problem, Uecker said, is that the standard allows a compiler to do\n*anything* in response to undefined behavior, up to the point of\ninvoking [nasal\ndemons](http://www.catb.org/jargon/html/N/nasal-demons.html).  If a program contains any undefined behavior at all, according\nto compiler writers, then it has no expected semantics.  The C++23 standard\ngoes further to explicitly state that the standard imposes no requirements\nfor these programs.  That has led to widespread disagreements between\ndevelopers about what can be expected from the language.\n\nFor example, if you zero an entire structure (perhaps with a call to [`memset()`](https://man7.org/linux/man-pages/man3/memset.3.html)),\nthen write to specific fields, what will happen if you read from any\npadding bytes in that structure?  Might they contain security-relevant\ndata?  [A\n2015 survey](https://www.cl.cam.ac.uk/~pes20/cerberus/notes50-survey-discussion.html) showed that there was no consensus on what should happen in\nthat case.  Or consider this simple code:\n\n\n```\n    extern int x;\n    int f(int a, int b)\n    {\n    \tx = b ? 42 : 43;\n\treturn a/b;\n    }\n```\nIf `b` is zero, then the `return` statement is a division by\nzero, which is undefined behavior.  In this case, is the compiler entitled\nto omit the test entirely and just execute `x = 42`?  After all, the\n`b = 0` case has no expected semantics, and can thus be ignored.\nThere are compilers that will do exactly that.  In the undefined-behavior\ncase, the store to `x` is not observable behavior.  But now consider\nthis case:\n\n\n```\n    extern void g(int x);\n    int f(int a, int b)\n    {\n        g(b ? 42 : 43);\n\treturn a/b;\n    }\n```\nThis might seem to be the same situation, with the compiler being entitled\nto remove the test and just pass 42 to `g()`, and some compilers\nhave treated that way — but that compiler behavior was a bug.  Imagine a\ndefinition of `g()` that calls `exit()` if `b` is\nzero.  In that case, the division will never happen and the program's\nbehavior is not undefined.  So eliding the test and simply passing 42 to\n`g()` is incorrect.\n\nOne more interesting case:\n\n\n```\n    volatile int x;\n    int foo(int a, int b, bool store_to_x)\n    {\n\tif (! store_to_x)\n\t    return a/b;\n\tx = b;\n\treturn a/b;\n    }\n```\nThe question here is: can the compiler hoist the final division operation\nabove assignment to `x`?  If there are no semantics associated with\nthe `b = 0` case, then there is no change in observable behavior.\nThis, too, is something compilers have done, but the C23 standard added a\n\"no time travel\" stipulation to disallow it.  In C++, instead, hoisting\nmust be explicitly prevented by inserting a call to\n`std::observable_checkpoint()`.\n\nTime-travel bugs should eventually go away, but there are a lot of other\nsituations where, even if the standard is clear, compiler writers often\ndisagree.  These include reading of uninitialized variables (which is\n*almost* always defined), and equality comparisons of pointers, which\nis always defined, but is also miscompiled by both Clang and GCC.\n\n\n#### Fighting undefined behavior\n\nTo try to address all of these problems and more, the C committee runs three study groups focused specifically on the memory object model, memory safety, and undefined behavior. There are currently about 100 instances of undefined behavior in the C standard, but the in-progress C2y draft has removed 45 of them. The situation is indeed getting better.\n\nThere is an increasingly rich set of tools aimed at finding issues: compiler warnings, static analyzers, sanitizers, LLM-based tools, formal verification, and more. The number of situations where a compiler will emit a warning where possible undefined behavior is detected is growing; recent examples include better warnings for integer overflows and potential use-after-free situations. Static analyzers are available as standalone tools, but are also increasingly being built into the compilers themselves; GCC can now warn about a number of potential buffer-overflow situations, for example. Sanitizers work by inserting run-time checks; they can catch a lot of undefined behavior and, in trapping mode, be used for hardening as well.\n\nMemory safety has never been one of C's strong points, but Uecker wanted to make the point that it can be improved. That problem breaks down into three sub-problems: type safety, spatial memory safety, and temporal memory safety.\n\nC, he said, has a strong type system, and the remaining problems are\nfixable.  Tagless unions, for example, can create type confusion, but the\ncompiler can enforce types with some additional annotations.  New\ndiagnostics can catch unsafe casts from `void`.  Type checking\nacross translation units is traditionally not a huge problem in C, since\nheader files are used to ensure consistent types, but the situation could\nbe improved with a link-time checker.\n\nSpatial memory safety — bounds checking — is a partially solved problem;\nthe compilers can perform array-bounds checking in many situations now.  In\nsome cases, some code changes are needed to fully benefit from this\nchecking.  Use of the [counted_by](https://lwn.net/Articles/936728/) attribute\ncan enable checking for flexible array members, for example.\n\nTemporal memory safety — avoiding use-after-free bugs and the like — is\nharder, Uecker said, and Rust definitely has an advantage there.  Still,\nbetter temporal memory-safety enforcement is possible.  Architectures like\n[CHERI](https://en.wikipedia.org/wiki/Capability_Hardware_Enhanced_RISC_Instructions)\ncan help here is well.  [Fil-C](https://lwn.net/Articles/1042938/) can find a\nlot of temporal-safety bugs.\n\nCan all of these tools and language changes get us to full memory safety? Completely solving the problem will require either expensive run-time checking or formal verification, he said. In the near future, the most complete results will be had with the combination of a restricted language and formal verification tools.\n\nOverall, he concluded, C is still a living language and is still improving.\nThe C23 standard removed a number of problematic features, including\nold-style (K&R) function definitions, support for sign-magnitude and\none's-complement machines, and trigraphs.  It added bit-precise integer\ntypes, checked integer operations, and more.  C2y will go further, adding\ncase ranges, named `for` loops, the `_Countof()` macro to\ndetermine array lengths, and a lot of \"demon removal\".  It will not\nachieve full memory safety for C, but that is an eventual possibility, and\nwill become more practical over time.  He ended by encouraging interested\npeople to participate in the working groups.\n\nThe [video](https://www.youtube.com/watch?v=dhlYcmX_oBQ&list=PLEVVgAZOU9P8&index=14) and [slides](https://uecker.codeberg.page/kernel-recipes.pdf) from this\ntalk are available.\n\n[Thanks to the Linux Foundation, LWN's travel sponsor, for supporting my\ntravel for this event.]","body_html":"<h1 id=\"reducing-undefined-behavior-in-the-c-language\">Reducing undefined behavior in the C language</h1>\n<p>By Jonathan Corbet, September 28, 2026</p>\n<p>As a professor of biomedical engineering, Martin Uecker perhaps does not fit the profile of a typical presenter at <a href=\"https://kernel-recipes.org/en/2026/\" rel=\"nofollow ugc noopener\">Kernel Recipes</a>. He is, however, a longtime Linux user, and works on free software for controlling magnetic resonance imaging (MRI) scanners. He was at the conference to talk about the C programming language, the specific problem of undefined behavior in C, and whether it can eventually be made into a memory-safe language.</p>\n<p>Why bother with C in 2026?  It is, he said, still a great language.  C is\nportable, stable over the long term, offers fast compilation, and the\nresulting binary code is fast.  &quot;What you see is what you get&quot;; it\nis easy to look at C code and have some idea of what the computer will\nactually do.  There are a lot of tools for working with the language, and C\ngets out of the way when necessary.</p>\n<p>C does have a long history, and that affects the language as we see it today, he said. The C89 standard had to cope with a wide variety of hardware, including machines with signed-magnitude or one&#39;s-complement integer representations, segmented memory, exotic pointer representations, and surprising sizes for types. Some Honeywell machines, for example, had nine-bit bytes. That greatly complicated the task of writing a standard that would enable the writing of portable code.</p>\n<p>The approach that was taken was to define the semantics of the language in\nterms of an abstract machine.  All operations are to be executed as if they\nhad run on that abstract machine, which may not exactly match the actual\nhardware.  The observable behavior of the program must be what the abstract\nmachine would have done.  The &quot;observable&quot; part matters: access to\n<code>volatile</code> variables, being defined as observable, must happen\nexactly according to the abstract machine; everything else just has to\nproduce the same eventual result.</p>\n<p>The standard gives a lot of freedom to compiler implementers; only the\nobservable behavior has to be preserved.  There are many aspects of that\nbehavior that are either undefined or implementation-defined.  These are\nnot observable behavior, and thus do not constrain what compiler\nimplementers can do.  There are, of course, other specifications that\n<em>can</em> constrain compiler developers where the C standard does not;\nthese include ABI requirements, standards like POSIX, or the need for\nbackward compatibility.</p>\n<p>Undefined behavior comes about when a program does something that is either\nnot portable or not defined by the standard at all.  In such cases, the C89\nstandard states that it &quot;imposes no requirements&quot; on the\nimplementation.  Undefined behavior exists for a number of reasons.\nIt allows implementations to support extensions, manage\ninteractions with hardware-based safety mechanisms, and perform aggressive\noptimization, all while allowing difficult-to-detect errors to be ignored.\nIt explicitly gives the compiler the right to ignore whole classes of\nhard-to-detect errors.</p>\n<h4 id=\"nasal-demons\">Nasal demons</h4>\n<p>The problem, Uecker said, is that the standard allows a compiler to do\n<em>anything</em> in response to undefined behavior, up to the point of\ninvoking <a href=\"http://www.catb.org/jargon/html/N/nasal-demons.html\" rel=\"nofollow ugc noopener\">nasal\ndemons</a>.  If a program contains any undefined behavior at all, according\nto compiler writers, then it has no expected semantics.  The C++23 standard\ngoes further to explicitly state that the standard imposes no requirements\nfor these programs.  That has led to widespread disagreements between\ndevelopers about what can be expected from the language.</p>\n<p>For example, if you zero an entire structure (perhaps with a call to <a href=\"https://man7.org/linux/man-pages/man3/memset.3.html\" rel=\"nofollow ugc noopener\"><code>memset()</code></a>),\nthen write to specific fields, what will happen if you read from any\npadding bytes in that structure?  Might they contain security-relevant\ndata?  <a href=\"https://www.cl.cam.ac.uk/~pes20/cerberus/notes50-survey-discussion.html\" rel=\"nofollow ugc noopener\">A\n2015 survey</a> showed that there was no consensus on what should happen in\nthat case.  Or consider this simple code:</p>\n<pre><code>    extern int x;\n    int f(int a, int b)\n    {\n        x = b ? 42 : 43;\n    return a/b;\n    }</code></pre>\n<p>If <code>b</code> is zero, then the <code>return</code> statement is a division by\nzero, which is undefined behavior.  In this case, is the compiler entitled\nto omit the test entirely and just execute <code>x = 42</code>?  After all, the\n<code>b = 0</code> case has no expected semantics, and can thus be ignored.\nThere are compilers that will do exactly that.  In the undefined-behavior\ncase, the store to <code>x</code> is not observable behavior.  But now consider\nthis case:</p>\n<pre><code>    extern void g(int x);\n    int f(int a, int b)\n    {\n        g(b ? 42 : 43);\n    return a/b;\n    }</code></pre>\n<p>This might seem to be the same situation, with the compiler being entitled\nto remove the test and just pass 42 to <code>g()</code>, and some compilers\nhave treated that way — but that compiler behavior was a bug.  Imagine a\ndefinition of <code>g()</code> that calls <code>exit()</code> if <code>b</code> is\nzero.  In that case, the division will never happen and the program&#39;s\nbehavior is not undefined.  So eliding the test and simply passing 42 to\n<code>g()</code> is incorrect.</p>\n<p>One more interesting case:</p>\n<pre><code>    volatile int x;\n    int foo(int a, int b, bool store_to_x)\n    {\n    if (! store_to_x)\n        return a/b;\n    x = b;\n    return a/b;\n    }</code></pre>\n<p>The question here is: can the compiler hoist the final division operation\nabove assignment to <code>x</code>?  If there are no semantics associated with\nthe <code>b = 0</code> case, then there is no change in observable behavior.\nThis, too, is something compilers have done, but the C23 standard added a\n&quot;no time travel&quot; stipulation to disallow it.  In C++, instead, hoisting\nmust be explicitly prevented by inserting a call to\n<code>std::observable_checkpoint()</code>.</p>\n<p>Time-travel bugs should eventually go away, but there are a lot of other\nsituations where, even if the standard is clear, compiler writers often\ndisagree.  These include reading of uninitialized variables (which is\n<em>almost</em> always defined), and equality comparisons of pointers, which\nis always defined, but is also miscompiled by both Clang and GCC.</p>\n<h4 id=\"fighting-undefined-behavior\">Fighting undefined behavior</h4>\n<p>To try to address all of these problems and more, the C committee runs three study groups focused specifically on the memory object model, memory safety, and undefined behavior. There are currently about 100 instances of undefined behavior in the C standard, but the in-progress C2y draft has removed 45 of them. The situation is indeed getting better.</p>\n<p>There is an increasingly rich set of tools aimed at finding issues: compiler warnings, static analyzers, sanitizers, LLM-based tools, formal verification, and more. The number of situations where a compiler will emit a warning where possible undefined behavior is detected is growing; recent examples include better warnings for integer overflows and potential use-after-free situations. Static analyzers are available as standalone tools, but are also increasingly being built into the compilers themselves; GCC can now warn about a number of potential buffer-overflow situations, for example. Sanitizers work by inserting run-time checks; they can catch a lot of undefined behavior and, in trapping mode, be used for hardening as well.</p>\n<p>Memory safety has never been one of C&#39;s strong points, but Uecker wanted to make the point that it can be improved. That problem breaks down into three sub-problems: type safety, spatial memory safety, and temporal memory safety.</p>\n<p>C, he said, has a strong type system, and the remaining problems are\nfixable.  Tagless unions, for example, can create type confusion, but the\ncompiler can enforce types with some additional annotations.  New\ndiagnostics can catch unsafe casts from <code>void</code>.  Type checking\nacross translation units is traditionally not a huge problem in C, since\nheader files are used to ensure consistent types, but the situation could\nbe improved with a link-time checker.</p>\n<p>Spatial memory safety — bounds checking — is a partially solved problem;\nthe compilers can perform array-bounds checking in many situations now.  In\nsome cases, some code changes are needed to fully benefit from this\nchecking.  Use of the <a href=\"https://lwn.net/Articles/936728/\" rel=\"nofollow ugc noopener\">counted_by</a> attribute\ncan enable checking for flexible array members, for example.</p>\n<p>Temporal memory safety — avoiding use-after-free bugs and the like — is\nharder, Uecker said, and Rust definitely has an advantage there.  Still,\nbetter temporal memory-safety enforcement is possible.  Architectures like\n<a href=\"https://en.wikipedia.org/wiki/Capability_Hardware_Enhanced_RISC_Instructions\" rel=\"nofollow ugc noopener\">CHERI</a>\ncan help here is well.  <a href=\"https://lwn.net/Articles/1042938/\" rel=\"nofollow ugc noopener\">Fil-C</a> can find a\nlot of temporal-safety bugs.</p>\n<p>Can all of these tools and language changes get us to full memory safety? Completely solving the problem will require either expensive run-time checking or formal verification, he said. In the near future, the most complete results will be had with the combination of a restricted language and formal verification tools.</p>\n<p>Overall, he concluded, C is still a living language and is still improving.\nThe C23 standard removed a number of problematic features, including\nold-style (K&amp;R) function definitions, support for sign-magnitude and\none&#39;s-complement machines, and trigraphs.  It added bit-precise integer\ntypes, checked integer operations, and more.  C2y will go further, adding\ncase ranges, named <code>for</code> loops, the <code>_Countof()</code> macro to\ndetermine array lengths, and a lot of &quot;demon removal&quot;.  It will not\nachieve full memory safety for C, but that is an eventual possibility, and\nwill become more practical over time.  He ended by encouraging interested\npeople to participate in the working groups.</p>\n<p>The <a href=\"https://www.youtube.com/watch?v=dhlYcmX_oBQ&amp;list=PLEVVgAZOU9P8&amp;index=14\" rel=\"nofollow ugc noopener\">video</a> and <a href=\"https://uecker.codeberg.page/kernel-recipes.pdf\" rel=\"nofollow ugc noopener\">slides</a> from this\ntalk are available.</p>\n<p>[Thanks to the Linux Foundation, LWN&#39;s travel sponsor, for supporting my\ntravel for this event.]</p>","headings":[{"level":1,"text":"Reducing undefined behavior in the C language","id":"reducing-undefined-behavior-in-the-c-language"}]}}