• lobsters__davmac@crustaceanson.theshelf.top
    link
    fedilink
    arrow-up
    0
    ·
    edit-2
    7 days ago

    Sure, if you write an actual std::unreachable() at a point in code that is quite clearly actually reachable, or take a literal null pointer and dereference it, the compiler may generate code that has what could be seen as pretty whack behaviour (and even that is happening less as there’s been more focus from the compiler engineers on avoiding it - witness the output of the GCC compiler that is referenced in the actual post). But coders generally don’t do these things, because they don’t make sense in the first place. I’m not saying these sort of thing absolutely never happen, but using them as examples of how UB can “always” have unpredictable results seems off.

    (As an aside, the existence of std::unreachable() is designed to make it easier for the compiler to optimise safely, i.e. it can emit trapping instructions such as ud2 in cases where it sees definite UB on some path, and omit those traps when the code has been marked as expectedly unreachable via std::unreachable(). I.e. there’s a reasonable alternative for the compiler to optimise other than by relying on the mere presence of UB to identify paths that won’t actually execute. For this reason, programmers need to be especially careful when using std::unreachable(); get it wrong and there really are no compiler-provided safety nets, unlike some other forms of UB where there may be. And already it should be clear that the linked post is vastly oversimplifying things).

    Anything involving pointers is pretty prone to horrible consequences if there’s UB - while a null pointer dereference typically does manifest in an immediate crash, things like buffer overrun and use-after-free can be much more subtle and much more insidious. This isn’t news. It also applies mostly to a very limited subset of causes of UB. Throwing all UB in the same bucket is really misleading, at best. You’ll occasionally see people (in reference to just how bad UB is) remark that even just having a missing newline at the end of a source file is UB. They always neglect to mention that it never, ever, actually causes the compiler to do anything significantly different than if the newline was present (beyond perhaps emitting a warning).

    I also strongly suspect that the vast majority (I’m saying “vast majority”, not all*) of actual runtime UB occurrences that don’t involve dereference of invalid pointers are either harmless in effect or at least do exactly what you would expect (eg integer overflow typically wraps if that’s what happens on the underlying architecture). The post is needlessly sensationalist, as was the preceding one.

    *: notable exceptions include two classics, “the compiler ate my overflow check” and “I logged the value of an uninitialised variable but nothing happened” - even the latter one should cease to be an issue going forward with language changes that have been coming down the pipeline


    score 1 · canonical

  • 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

    • 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

            • 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.


              score 1 · canonical

              • Nowhere in the “C semantics” does it say that a compiler must create nasal demons for every instance of UB being invoked.

                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

    • 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.

      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 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?


      score 1 · 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++.

        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

        • 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.

          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):

          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.


          score 1 · canonical

      • lobsters__mort@crustaceanson.theshelf.topM
        link
        fedilink
        arrow-up
        0
        ·
        edit-2
        7 days ago

        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 struct sockaddr_in addr has the effective type of struct sockaddr_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 unsigned char *. 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.

        Maybe I should write a blog post about this…


        score 1 · canonical

        • lobsters__pervognsen@crustaceanson.theshelf.topM
          link
          fedilink
          arrow-up
          0
          ·
          edit-2
          7 days ago

          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.


          score 1 · canonical

      • 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.


        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.

          Fully agreed. :) Shameless plug: https://www.ralfj.de/blog/2021/11/18/ub-good-idea.html

          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.

          Fil-C goes a lot further than that.


          score 1 · canonical

  • I give up. I do not understand why people responded to my comment justifying why they want to use UB as if my complaint were them doing that. My complaint is very simple: something being UB means an implementation can do whatever it wants. It does not mean that you, the developer, can rely on that behavior.

    That’s literally what UB means.

    I did not, at any point say “you should not be allowed to rely on this”.

    I said that by labeling such operations as UB, C and C++ said that you are not allowed to.

    In every one of these cases, all you need to do is convince the C and C++ committees to just define the behavior. Doing that simply requires replacing “is undefined” with (or actually saying at ll" “is implementation definedunspecified|erroneous behavior.” Those changes instantly gives you what you want, rather than what you are actually being given.

    Anyway, I’m done talking into a wall of people rejecting the plain language of the spec, and the demonstrable behavior of C and C++ compilers when reasoning about UB operations. Instead I’ll focus on actually improving the standards instead.


    score 1 · canonical

    • That’s literally what UB means.

      The problem is that you’re assuming we don’t know what it means. It’s almost infantilizing. The people who know C well enough to complain about UB are ones who actually understand what it means.

      In every one of these cases, all you need to do is convince the C and C++ committees to just define the behavior.

      Right, because that’s so easy. It’s well known that the C and C++ WG are full of politiking. You’re being facetious. Not everyone can be in the working groups, but they can complain online. The hope is that with enough complaints, someone who is in the working groups does something. Same as politics in real life. Not everyone is cut out to be a Congress Critter, but they can hold a megaphone online.


      score 1 · canonical