After my post on undefined behavior in C some people insisted that it’s demonstrably fine, we know what the code will do, and at worst we’ll get a segfault.
Seriously. People need to stop thinking UB is about allowing different behavior.
It is absolutely not. No amount of arguing changes this.
UB is not implementation defined behavior. That term already exists.
UB places no constraints on the compiler. If the standard does not define the result of doing X in condition Y is undefined, or it just does not define the result (either explicitly, ID, unspecified, or erroneous), then there are no constraints on the compiler.
It does not matter what your implementation, platform, or hardware do. If the behavior is UB according to the language, the compiler can do anything, or nothing, or different things in different places. If the language says it is UB, it is UB.
If you want different implementations to be able to provide different behavior, you want implementation defined behavior. That is literally why ID exists separately from UB.
some people insisted that it’s demonstrably fine, we know what the code will do, and at worst we’ll get a segfault. This is false.
UB places no constraints on the compiler.
I think it comments like yours and posts like this contribute to an extremely negative sentiment around the state of undefined behaviour in languages like C and C++. I don’t mean this to imply skepticism or suggest blame, but to state, as neutrally as I can, that examples like those shared in the article are genuinely alarming, and the consequence is negativity.
(The “falling through a function” example comes up a lot, probably because it is very simply yet very provocative, though I think -fsanitize=unreachable may handle this problem, judging from its effect on the Godbolt example the author provides. There appear to be other flags I can use to catch other, similar examples, and while these may be worth mentioning, I don’t know that they materially change the conclusions of the article.)
I think this negativity results in some wondering whether the compiler folks have gone mad, but in previous discussions of undefined behaviour, comments from folks like ~david_chisnall or ~lonjil suggest that manifestations of undefined behaviour are not a result of some negligence or some malicious intent.
e.g.,
it is impossible to write an optimizing compiler that behaves sensibly in the presence of UB. The only real solution, which pretty much every programming language in the world other than C and C++ do, is to make it harder or even impossible to have UB in your code.
As an aside, when authors like ~matklad discuss memory safety, they consider undefined behaviour in their definitions, though this leads to conclusions that I’m not sure I agree with, such as:
This is useless! Here, I’ve just made a language called Lil-C, which exactly like C, except that every UB is formally defined to trap. It is safe! And it can run any C program! Have I just done anything useful? No!
This actually seems like it would be quite useful…?
Anyway, by this post and the above comment’s reasoning, it is not possible to constrain the consequence of undefined behaviour; as a result, it is not possible to retroäctively detect that it has performed an undesirable transformation. However, since undefined behaviour arises as an emergent property across multiple (desirable, reasonably optimisations,) I also cannot simply read through a lists of undefined behaviours in C and expect to be able to avoid triggered it intentionally. Understandably frustrating!
But, as a human being, I am allowed (if not expected) to have contradictory desires, and to want all benefit with no sacrifice. I want my programming language to be extremely simple, but also extremely fast, but also very safe, but also without undefined behaviour, but also without giving up optimisations that other languages (less safe or more rife with undefined behaviour) are allowed, and I might be convinced not to write any new codes in C or C++ (in fact, I have found myself running “toward” Zig more than “away” from C,) but I definitely want to make substantive changes to existing codes in C and C++ and refuse to replace or rewrite them. (Regarding this last desire, you know, I figure I already dug the hole… and I’m not going to lose sleep over digging it just a little bit deeper…)
So, rather than simply being negative about undefined behaviour or being negative about C and C++ code (that I refuse to do much of anything about…,) perhaps what would be useful to know would be how I can actually go about triangulating myself. I know I’m in a hole, I know I am digging it deeper, I know I won’t climb out, and I know I might hit a pipe… but before the water rises up to my neck, is there no agency I can have in my last moments?
In other words, even if I won’t rewrite my C codes, and even if I do want to make substantive changes to them, and even if I know I will suffer from the consequences of undefined behaviour, and even if these will be difficult to avoid through additional mechanism or additional diligence, I still have some obligation-to and corresponding freedom-of action. What do I do in the next (… maybe the last? … hopefully the last?) hundred thousand lines of C that I write? Just flail and drown?
I think it comments like yours and posts like this contribute to an extremely negative sentiment around the state of undefined behaviour in languages like C and C++.
It is not being “negative”, it is being precise. The sentiment of “I can do UB if I know what the hardware does” is way too wide-spread. For example, even in the early days of Rust we had code in major crates that performed unaligned loads on x86 saying that it’s fine on this hardware, and in C this remains a common pattern – but it’s UB and clang/LLVM will “miscompile” such code under the right circumstances.
Maybe you have already understood all this and hence find the examples in the post needlessly alarming. But the sad reality is that many, many programmers have not, and in fact C is still taught to people in a way that gives them the wrong idea about UB. Until the majority of C/C++/Rust programmers has learnt to take UB seriously and to treat it with precision, we will need posts and comments like the GP. (I think in Rust we are doing quite well here, but we’ll never really be done educating people.)
A precise treatment of UB can include “get the compiler to document that some case is not UB”. As people like to point out in these discussions, compilers are totally allowed to refine the standard and define behavior that the standard leaves as UB. They don’t have to wait for the committee to force them to remove UB. However, to my knowledge, neither GCC nor clang do this. Without a clearly defined contract, it becomes impossible to precisely analyze whether a piece of code actually is correct or not. Sanitizers, hardened compilation modes, UB-checking tools like Miri – all of those can only work of there is a precise agreed-upon contract around UB. The lack of such a contract for C and C++ is really the main problem in my eyes: the standard leaves too many things undefined that practical systems programming needs, and compiler vendors do not bother to document any extra guarantees. Both sides are to be blamed for the mess that we find ourselves in as a result.
compilers are totally allowed to refine the standard and define behavior that the standard leaves as UB. They don’t have to wait for the committee to force them to remove UB. However, to my knowledge, neither GCC nor clang do this.
Not all of these are UB in standard C and are just ill-formed, but at least arithmetic on void* is UB, to my understanding. The C23 standard draft says in 6.5.7.2 (emphasis mine):
For addition, either both operands shall have arithmetic type, or one operand shall be a pointer to a complete object type and the other shall have integer type
Where 6.2.5.24 says:
The void type comprises an empty set of values; it is an incomplete object type that cannot be
completed.
And then 4.2:
If a “shall” or “shall not” requirement that appears outside of a constraint or runtime-constraint is
violated, the behavior is undefined.
Beyond these, both GCC and Clang document optional extensions that remove some UB if you turn them on, e.g. -wrapv and -ftrapv making signed integer overflow well-defined.
Note that 6.5.7p2 is under a “constraints” heading, so breaking those requirements (ie, attempting arithmetic on a pointer to an incomplete type, including void *) is a compile-time error.
Your complaint is … you agree that UB has the qualities outlined by TFA, ~olliej and others, but you think the comments should be more positive somehow?
What you can do is: learn C. Learn what the standard guarantees, what choices it leaves up to implementations (implementation-defined behavior) and what it labels undefined behavior. Then try to avoid writing code which invokes undefined behavior. That’s it. Don’t do what so many wants to do and dismiss it because “I only target one platform” because it’s not that simple.
Here’s a few notes about types and aliasing:
Every object has an “effective type”. The effective type of stack-allocated objects have the type you declared them as (e.g an int foo has effective type of int, a structsockaddr_in addr has the effective type of structsockaddr_in).
Heap-allocated memory starts out without an effective address (yes, malloc is magic; you may treat memory obtained from mmap the same way). They gain an effective type the moment you write anything to it (either by writing through a pointer or through functions like memcpy).
Do not dereference an object of effective type X through a pointer type of type Y. If you have an int, don’t dereference it through a pointer to float. There are two exceptions:
You can look at the memory of any object by going through a char * or unsignedchar *. This is how e.g memcpy can copy bytes without knowing the effective type.
Note that this does not work the other way: if you have an object of effective type char (e.g as a part of a stack-allocated array char[N]), you may not access it through a pointer to some other type. This has implications for how you can use stack-allocated buffers.
If you have two struct types X and Y with a “common initial sequence” (meaning the first few fields are the same types in the same order), you may have an object with effective type X and look at those common initial fields through a pointer to Y.
The consequence of this is that you should be using memcpy a lot more; if you have some memory buffer and you know there’s an int at offset K, don’t make an int * to it, instead make a local int variable and memcpy the data. The compiler will figure out how to make it fast.
Then there are the obvious ones: don’t dereference null pointers, don’t overflow integers (not even through addition or bit shifting), don’t write outside of memory you allocated.
There’s more of course, this is just what I remember off the top of my head right now as things which might catch people off guard if they learned about C by learning that “memory is a big flat array of bytes and pointers are indexes into that array”. That is true when you’re implementing C but not when you’re programming in C.
Then there are kinds of UB which you can safely ignore for a given platform. For example, ISO C disallows casting between an object pointer (such as void *) and a function pointer; however the POSIX dynamic linker API provides one function dlsym which returns a void *, which the caller is expected to cast to a function pointer type and then call. As a result, every implementation which supports the POSIX dynamic linker API allows casting between object pointers and function pointers. And both GCC and Clang (and probably others) explicitly allow you to violate the effective type rules by going through a union.
yes, malloc is magic; you may treat memory obtained from mmap the same way
As for how you can justify and think about this interpretation of mmap in terms of the existing abstract machine semantics of malloc, I really like @ralfj’s exposition at https://www.ralfj.de/blog/2026/03/13/inline-asm.html. I believe a possible formalization of storytelling is something like the FFI oracle in CakeML’s semantics. Although a language like CakeML doesn’t have to contend with the complexity of a C-like or Rust-like memory model, the idea should generalize to whenever you’re trying to bridge two alien worlds and you can explain the locally visible effect of the remote world in terms of local semantics. What seems hard is coming up with the right explanations for tricky examples, as in Ralf’s article. There’s a concept I’m originally familiar with from linearizability where you’re allowed to explain a final set of observations retroactively, which allows for and often requires similar creativity.
I love the analogy to linearizability (specifically, the freedom to retroactively adjust the linearization point, e.g. fixing the linearization point of a write following a subsequent read).
it is impossible to write an optimizing compiler that behaves sensibly in the presence of UB. The only real solution, which pretty much every programming language in the world other than C and C++ do, is to make it harder or even impossible to have UB in your code.
In fact, authors like Chris Lattner have even been quite explicit in pointing out “advantages of undefined behaviour in C”
UB is really useful to languages such as C. The key point with say Rust, is that it’s basically like C, useful in the same situation, and has tons of undefined behavior which is really useful. But you can’t actually hit any UB without using unsafe {}. We get to have the advantages of UB while making things much easier for the programmer.
This is useless! Here, I’ve just made a language called Lil-C, which exactly like C, except that every UB is formally defined to trap. It is safe! And it can run any C program! Have I just done anything useful? No!
This actually seems like it would be quite useful…?
I disagree with Matklad, but only to a degree. If you specified such a thing, but had no way to run it efficiently, it would be of limited usefulness. Good to run some old non-performance-critical codebase you don’t want to port to another language, but useless for anything who has a reason to choose to use C today. If it’s fast, like if you have CHERI hardware, then it’d be a lot more useful.
Anyway, by this post and the above comment’s reasoning, it is not possible to constrain the consequence of undefined behaviour; as a result, it is not possible to retroäctively detect that it has performed an undesirable transformation. However, since undefined behaviour arises as an emergent property across multiple (desirable, reasonably optimisations,) I also cannot simply read through a lists of undefined behaviours in C and expect to be able to avoid triggered it intentionally. Understandably frustrating!
it is not possible to retroäctively detect that it has performed an undesirable transformation
I don’t think it’s possible to even know what is or isn’t a desire transformation in general.
However, since undefined behaviour arises as an emergent property across multiple (desirable, reasonably optimisations,)
UB doesn’t arise from anything like that. Whether code is UB is an inherent property of the code and the language spec. And since the language spec isn’t a formal mathematical model, it’s sometimes very difficult to know what is well-defined.
Even with zero fancy optimization passes, you’d still suffer! A stray ińvalid write can still mess with your return pointer, or some important code in libc, or whatever, and totally mess up your program.
In other words, even if I won’t rewrite my C codes, and even if I do want to make substantive changes to them, and even if I know I will suffer from the consequences of undefined behaviour, and even if these will be difficult to avoid through additional mechanism or additional diligence, I still have some obligation-to and corresponding freedom-of action. What do I do in the next (… maybe the last? … hopefully the last?) hundred thousand lines of C that I write? Just flail and drown?
I would say that C and C++ are simply completely horrible languages that ought to cease to exist. In practice, this is difficult. Perhaps the easiest option would be to elect political leaders all over the world who are on board with mandating CHERI for all everything.
UB is really useful to languages such as C. The key point with say Rust, is that it’s basically like C, useful in the same situation, and has tons of undefined behavior which is really useful.
I disagree with Matklad, but only to a degree. If you specified such a thing, but had no way to run it efficiently, it would be of limited usefulness. Good to run some old non-performance-critical codebase you don’t want to port to another language, but useless for anything who has a reason to choose to use C today. If it’s fast, like if you have CHERI hardware, then it’d be a lot more useful.
Note that CHERI-C is not “C with all UB removed or replaced by a trap”. (I know you didn’t say it is, but it is a common misconception.) CHERI safeguards only kick in after the compiler already did all its optimizations, so UB can still cause nasal demons in the internal logic of the program (like, say, a variable having a value it was never assigned). CHERI will, however, ensure that that the nasal demons are confined, similar to if you compile C to wasm and run that.
An implementation is allowed to provide a definition for what the C standard leaves implicitly or explicitly undefined. There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
This is not to say that all UB is like this, or that all people who write C and introduce UB into their code know what they’re doing, but some people do know what they’re doing.
If you are writing C and you are writing C against one of these other standards, or specifically against a specific implementation, there is nothing wrong with relying on the specific explicit definitions of behaviour which may otherwise be undefined, as long as you’re doing it consciously.
(I maintain, however, that nobody that is looking for advice on whether they should write C should not be writing C. The people who are writing C and are not looking or advice on whether they should continue to write it should also seriously consider moving to something with fewer footguns if they can.)
There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
And that does not matter.
I C says an API is UB if used in a specific way, it does not matter what the API implementation does or says. If the C standard says that is UB, it puts no requirements on the compiler.
There is no difference between using an API that posix says is defined but C says is not vs dividing by zero, or overflowing an integer.
If you write C code that depends on implementation defined behavior (POSIX), but C says is UB, your C code is wrong.
The fact that you can get away with it today, doesn’t magically make your code correct. It does not mean that it will work if you change header orders, or inlining changes, or anything else.
If your code invokes UB according to the language, you are not writing conforming C. Maybe your compiler allows it (for example -fwrapv, -ftrapv, -fno-delete-null-pointer-checks, etc) but that is no longer conforming C, and your program is wrong vs the standard: any other C compiler is free to assume your UB code does not occur, or is unreachable, etc.
What elevates the C standard to this special status above other standards such as POSIX and other documentation?
In my opinion, it’s nothing. If you’re targeting the C standard, its rules apply. If you’re targeting POSIX, you get to rely on its guarantees. If you’re targeting glibc, you get to rely on its guarantees.
The C standard is not the law. Heck, it’s not even a certification. C compilers can remain noncompliant for decades and face no consequences at all.
It isn’t special. C has its own defined language semantics.
POSIX can have its own.
If you want to use POSIX APIs in accordance to the POSIX standard, even if that conflicts with C, you just need to accept you are (1) not programming in C, and (2) if hoping the compiler is choosing to not apply C semantics.
If you want to use POSIX APIs in accordance to the POSIX standard, even if that conflicts with C, you just need to accept you are (1) not programming in C, and (2) if hoping the compiler is choosing to not apply C semantics.
This is crossing into genuine nonsense territory.
Nowhere in the “C semantics” does it say that a compiler must create nasal demons for every instance of UB being invoked. POSIX semantics do not “conflict with C” when they define some specific behavior that’s been left undefined by ISO C.
If I’m using POSIX APIs in accordance with the POSIX standard, and using a C toolchain that implements said POSIX standard, I do not need to hope for anything, and the toolchain has no freedom to choose: if the behavior of a particular operation is undefined in ISO C, but is very much defined by POSIX, then POSIX wins. Same with everything else.
In no way my reply above could be interpreted as you’ve just interpreted it.
It seems that you are trying to twist every possible consideration into something to support your position, even when it is clearly nonsensical. Please stop doing that.
You literally started your comment with an accusation of me stating genuine nonsense, so please don’t accuse me of bad faith. Especially when you followed with this:
If I’m using POSIX APIs in accordance with the POSIX standard, and using a C toolchain that implements said POSIX standard, I do not need to hope for anything, and the toolchain has no freedom to choose: if the behavior of a particular operation is undefined in ISO C, but is very much defined by POSIX, then POSIX wins. Same with everything else.
If C also defines a method, which for many “POSIX” APIs it does, then the C standard definitions of those APIs (things like malloc, realloc, free, etc) are determined by the C standard. POSIX is merely an implementation of those C interfaces, the provides its own extensions to the C interface. Unfortunately instead of those extensions being governed by implementation defined behavior (POSIX definition wins), they’re labeled UB in the standard (POSIX definition is irrelevant).
Anyway. I realize comments like this keep pulling me into this nonsense thread where people seem hell bent on acting like UB is safe if the implementation provides defined behavior, when literally the entire point of my first comment is that every single one of the expectations that people present is instantly resolved by the standard saying something is implementation defined, not UB.
Seriously. People need to stop thinking UB is about allowing different behavior.
It is absolutely not. No amount of arguing changes this.
UB is not implementation defined behavior. That term already exists.
UB places no constraints on the compiler. If the standard does not define the result of doing X in condition Y is undefined, or it just does not define the result (either explicitly, ID, unspecified, or erroneous), then there are no constraints on the compiler.
It does not matter what your implementation, platform, or hardware do. If the behavior is UB according to the language, the compiler can do anything, or nothing, or different things in different places. If the language says it is UB, it is UB.
If you want different implementations to be able to provide different behavior, you want implementation defined behavior. That is literally why ID exists separately from UB.
score 3 · canonical
Still dealing with senior engineers arguing that signed overflow in C/C++ is fine on x86 since the arch defines it…
score 2 · canonical
I think it comments like yours and posts like this contribute to an extremely negative sentiment around the state of undefined behaviour in languages like C and C++. I don’t mean this to imply skepticism or suggest blame, but to state, as neutrally as I can, that examples like those shared in the article are genuinely alarming, and the consequence is negativity.
(The “falling through a function” example comes up a lot, probably because it is very simply yet very provocative, though I think
-fsanitize=unreachablemay handle this problem, judging from its effect on the Godbolt example the author provides. There appear to be other flags I can use to catch other, similar examples, and while these may be worth mentioning, I don’t know that they materially change the conclusions of the article.)I think this negativity results in some wondering whether the compiler folks have gone mad, but in previous discussions of undefined behaviour, comments from folks like ~david_chisnall or ~lonjil suggest that manifestations of undefined behaviour are not a result of some negligence or some malicious intent.
e.g.,
In fact, authors like Chris Lattner have even been quite explicit in pointing out “advantages of undefined behaviour in C”.
As an aside, when authors like ~matklad discuss memory safety, they consider undefined behaviour in their definitions, though this leads to conclusions that I’m not sure I agree with, such as:
This actually seems like it would be quite useful…?
Anyway, by this post and the above comment’s reasoning, it is not possible to constrain the consequence of undefined behaviour; as a result, it is not possible to retroäctively detect that it has performed an undesirable transformation. However, since undefined behaviour arises as an emergent property across multiple (desirable, reasonably optimisations,) I also cannot simply read through a lists of undefined behaviours in C and expect to be able to avoid triggered it intentionally. Understandably frustrating!
But, as a human being, I am allowed (if not expected) to have contradictory desires, and to want all benefit with no sacrifice. I want my programming language to be extremely simple, but also extremely fast, but also very safe, but also without undefined behaviour, but also without giving up optimisations that other languages (less safe or more rife with undefined behaviour) are allowed, and I might be convinced not to write any new codes in C or C++ (in fact, I have found myself running “toward” Zig more than “away” from C,) but I definitely want to make substantive changes to existing codes in C and C++ and refuse to replace or rewrite them. (Regarding this last desire, you know, I figure I already dug the hole… and I’m not going to lose sleep over digging it just a little bit deeper…)
So, rather than simply being negative about undefined behaviour or being negative about C and C++ code (that I refuse to do much of anything about…,) perhaps what would be useful to know would be how I can actually go about triangulating myself. I know I’m in a hole, I know I am digging it deeper, I know I won’t climb out, and I know I might hit a pipe… but before the water rises up to my neck, is there no agency I can have in my last moments?
In other words, even if I won’t rewrite my C codes, and even if I do want to make substantive changes to them, and even if I know I will suffer from the consequences of undefined behaviour, and even if these will be difficult to avoid through additional mechanism or additional diligence, I still have some obligation-to and corresponding freedom-of action. What do I do in the next (… maybe the last? … hopefully the last?) hundred thousand lines of C that I write? Just flail and drown?
score 1 · canonical
It is not being “negative”, it is being precise. The sentiment of “I can do UB if I know what the hardware does” is way too wide-spread. For example, even in the early days of Rust we had code in major crates that performed unaligned loads on x86 saying that it’s fine on this hardware, and in C this remains a common pattern – but it’s UB and clang/LLVM will “miscompile” such code under the right circumstances.
Maybe you have already understood all this and hence find the examples in the post needlessly alarming. But the sad reality is that many, many programmers have not, and in fact C is still taught to people in a way that gives them the wrong idea about UB. Until the majority of C/C++/Rust programmers has learnt to take UB seriously and to treat it with precision, we will need posts and comments like the GP. (I think in Rust we are doing quite well here, but we’ll never really be done educating people.)
A precise treatment of UB can include “get the compiler to document that some case is not UB”. As people like to point out in these discussions, compilers are totally allowed to refine the standard and define behavior that the standard leaves as UB. They don’t have to wait for the committee to force them to remove UB. However, to my knowledge, neither GCC nor clang do this. Without a clearly defined contract, it becomes impossible to precisely analyze whether a piece of code actually is correct or not. Sanitizers, hardened compilation modes, UB-checking tools like Miri – all of those can only work of there is a precise agreed-upon contract around UB. The lack of such a contract for C and C++ is really the main problem in my eyes: the standard leaves too many things undefined that practical systems programming needs, and compiler vendors do not bother to document any extra guarantees. Both sides are to be blamed for the mess that we find ourselves in as a result.
score 1 · canonical
At least GCC lists a couple of these here: https://gcc.gnu.org/onlinedocs/gcc/Semantic-Extensions.html
Not all of these are UB in standard C and are just ill-formed, but at least arithmetic on void* is UB, to my understanding. The C23 standard draft says in 6.5.7.2 (emphasis mine):
Where 6.2.5.24 says:
And then 4.2:
Beyond these, both GCC and Clang document optional extensions that remove some UB if you turn them on, e.g.
-wrapvand-ftrapvmaking signed integer overflow well-defined.score 1 · canonical
Note that 6.5.7p2 is under a “constraints” heading, so breaking those requirements (ie, attempting arithmetic on a pointer to an incomplete type, including void *) is a compile-time error.
score 1 · canonical
Damn, thanks for catching that, I clearly wasn’t paying enough attention. So it is a language extension, but not one that removes UB.
score 1 · canonical
Your complaint is … you agree that UB has the qualities outlined by TFA, ~olliej and others, but you think the comments should be more positive somehow?
What you can do is: learn C. Learn what the standard guarantees, what choices it leaves up to implementations (implementation-defined behavior) and what it labels undefined behavior. Then try to avoid writing code which invokes undefined behavior. That’s it. Don’t do what so many wants to do and dismiss it because “I only target one platform” because it’s not that simple.
Here’s a few notes about types and aliasing:
int foohas effective type of int, astruct sockaddr_in addrhas the effective type ofstruct sockaddr_in).mallocis magic; you may treat memory obtained frommmapthe same way). They gain an effective type the moment you write anything to it (either by writing through a pointer or through functions like memcpy).char *orunsigned char *. This is how e.g memcpy can copy bytes without knowing the effective type.char(e.g as a part of a stack-allocated arraychar[N]), you may not access it through a pointer to some other type. This has implications for how you can use stack-allocated buffers.The consequence of this is that you should be using
memcpya lot more; if you have some memory buffer and you know there’s an int at offset K, don’t make anint *to it, instead make a local int variable and memcpy the data. The compiler will figure out how to make it fast.Then there are the obvious ones: don’t dereference null pointers, don’t overflow integers (not even through addition or bit shifting), don’t write outside of memory you allocated.
There’s more of course, this is just what I remember off the top of my head right now as things which might catch people off guard if they learned about C by learning that “memory is a big flat array of bytes and pointers are indexes into that array”. That is true when you’re implementing C but not when you’re programming in C.
Then there are kinds of UB which you can safely ignore for a given platform. For example, ISO C disallows casting between an object pointer (such as
void *) and a function pointer; however the POSIX dynamic linker API provides one functiondlsymwhich returns avoid *, which the caller is expected to cast to a function pointer type and then call. As a result, every implementation which supports the POSIX dynamic linker API allows casting between object pointers and function pointers. And both GCC and Clang (and probably others) explicitly allow you to violate the effective type rules by going through a union.Maybe I should write a blog post about this…
score 1 · canonical
As for how you can justify and think about this interpretation of mmap in terms of the existing abstract machine semantics of malloc, I really like @ralfj’s exposition at https://www.ralfj.de/blog/2026/03/13/inline-asm.html. I believe a possible formalization of storytelling is something like the FFI oracle in CakeML’s semantics. Although a language like CakeML doesn’t have to contend with the complexity of a C-like or Rust-like memory model, the idea should generalize to whenever you’re trying to bridge two alien worlds and you can explain the locally visible effect of the remote world in terms of local semantics. What seems hard is coming up with the right explanations for tricky examples, as in Ralf’s article. There’s a concept I’m originally familiar with from linearizability where you’re allowed to explain a final set of observations retroactively, which allows for and often requires similar creativity.
score 1 · canonical
I love the analogy to linearizability (specifically, the freedom to retroactively adjust the linearization point, e.g. fixing the linearization point of a write following a subsequent read).
score 1 · canonical
UB is really useful to languages such as C. The key point with say Rust, is that it’s basically like C, useful in the same situation, and has tons of undefined behavior which is really useful. But you can’t actually hit any UB without using
unsafe {}. We get to have the advantages of UB while making things much easier for the programmer.I disagree with Matklad, but only to a degree. If you specified such a thing, but had no way to run it efficiently, it would be of limited usefulness. Good to run some old non-performance-critical codebase you don’t want to port to another language, but useless for anything who has a reason to choose to use C today. If it’s fast, like if you have CHERI hardware, then it’d be a lot more useful.
I don’t think it’s possible to even know what is or isn’t a desire transformation in general.
UB doesn’t arise from anything like that. Whether code is UB is an inherent property of the code and the language spec. And since the language spec isn’t a formal mathematical model, it’s sometimes very difficult to know what is well-defined.
Even with zero fancy optimization passes, you’d still suffer! A stray ińvalid write can still mess with your return pointer, or some important code in libc, or whatever, and totally mess up your program.
I would say that C and C++ are simply completely horrible languages that ought to cease to exist. In practice, this is difficult. Perhaps the easiest option would be to elect political leaders all over the world who are on board with mandating CHERI for all everything.
score 1 · canonical
Fully agreed. :) Shameless plug: https://www.ralfj.de/blog/2021/11/18/ub-good-idea.html
Note that CHERI-C is not “C with all UB removed or replaced by a trap”. (I know you didn’t say it is, but it is a common misconception.) CHERI safeguards only kick in after the compiler already did all its optimizations, so UB can still cause nasal demons in the internal logic of the program (like, say, a variable having a value it was never assigned). CHERI will, however, ensure that that the nasal demons are confined, similar to if you compile C to wasm and run that.
Fil-C goes a lot further than that.
score 1 · canonical
An implementation is allowed to provide a definition for what the C standard leaves implicitly or explicitly undefined. There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
This is not to say that all UB is like this, or that all people who write C and introduce UB into their code know what they’re doing, but some people do know what they’re doing.
If you are writing C and you are writing C against one of these other standards, or specifically against a specific implementation, there is nothing wrong with relying on the specific explicit definitions of behaviour which may otherwise be undefined, as long as you’re doing it consciously.
(I maintain, however, that nobody that is looking for advice on whether they should write C should not be writing C. The people who are writing C and are not looking or advice on whether they should continue to write it should also seriously consider moving to something with fewer footguns if they can.)
score 1 · canonical
You cannot write code that depends on UB.
There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
And that does not matter.
I C says an API is UB if used in a specific way, it does not matter what the API implementation does or says. If the C standard says that is UB, it puts no requirements on the compiler.
There is no difference between using an API that posix says is defined but C says is not vs dividing by zero, or overflowing an integer.
If you write C code that depends on implementation defined behavior (POSIX), but C says is UB, your C code is wrong.
The fact that you can get away with it today, doesn’t magically make your code correct. It does not mean that it will work if you change header orders, or inlining changes, or anything else.
If your code invokes UB according to the language, you are not writing conforming C. Maybe your compiler allows it (for example -fwrapv, -ftrapv, -fno-delete-null-pointer-checks, etc) but that is no longer conforming C, and your program is wrong vs the standard: any other C compiler is free to assume your UB code does not occur, or is unreachable, etc.
score 1 · canonical
What elevates the C standard to this special status above other standards such as POSIX and other documentation?
In my opinion, it’s nothing. If you’re targeting the C standard, its rules apply. If you’re targeting POSIX, you get to rely on its guarantees. If you’re targeting glibc, you get to rely on its guarantees.
The C standard is not the law. Heck, it’s not even a certification. C compilers can remain noncompliant for decades and face no consequences at all.
score 2 · canonical
It isn’t special. C has its own defined language semantics.
POSIX can have its own.
If you want to use POSIX APIs in accordance to the POSIX standard, even if that conflicts with C, you just need to accept you are (1) not programming in C, and (2) if hoping the compiler is choosing to not apply C semantics.
score 1 · canonical
This is crossing into genuine nonsense territory.
Nowhere in the “C semantics” does it say that a compiler must create nasal demons for every instance of UB being invoked. POSIX semantics do not “conflict with C” when they define some specific behavior that’s been left undefined by ISO C.
If I’m using POSIX APIs in accordance with the POSIX standard, and using a C toolchain that implements said POSIX standard, I do not need to hope for anything, and the toolchain has no freedom to choose: if the behavior of a particular operation is undefined in ISO C, but is very much defined by POSIX, then POSIX wins. Same with everything else.
score 1 · canonical
My god you’re right, that’s why we don’t need to worry about compilers removing null checks, or pretending integer overflow doesn’t occur, right?
score 1 · canonical
In no way my reply above could be interpreted as you’ve just interpreted it.
It seems that you are trying to twist every possible consideration into something to support your position, even when it is clearly nonsensical. Please stop doing that.
score 1 · canonical
You literally started your comment with an accusation of me stating genuine nonsense, so please don’t accuse me of bad faith. Especially when you followed with this:
If C also defines a method, which for many “POSIX” APIs it does, then the C standard definitions of those APIs (things like malloc, realloc, free, etc) are determined by the C standard. POSIX is merely an implementation of those C interfaces, the provides its own extensions to the C interface. Unfortunately instead of those extensions being governed by implementation defined behavior (POSIX definition wins), they’re labeled UB in the standard (POSIX definition is irrelevant).
Anyway. I realize comments like this keep pulling me into this nonsense thread where people seem hell bent on acting like UB is safe if the implementation provides defined behavior, when literally the entire point of my first comment is that every single one of the expectations that people present is instantly resolved by the standard saying something is implementation defined, not UB.
So I’m good.
score 0 · canonical