102 comments

[ 0.23 ms ] story [ 154 ms ] thread
Unpopular opinion: the problem isn't so much undefined behaviour, but rather that compiler vendors have chosen to exploit undefined behaviour in their optimization passes.

PS: some pragmatic cleanup of UB and IB in the C standard would be welcome though.

This is not an unpopular opinion per se, but one that puts you firmly in the "semi-portable C" camp (in the "camp" model of the essay I linked in another comment), while the compiler vendors themselves have moved entirely away from that camp into the "standard C" camp. As I wrote, "The principles and practices in one camp can seem alien, even threatening to another."
How would you modify the standard and/or optimization passes for the first example in the article?
@mastax, you could model pointer dereferencing as an effect and require dereferencing null pointers to trap.
You can't remove A (redundant given B) and then subsequently remove B (redundant) as well.

As soon as your first pass decides to rely on the presence of the dereference as a reason for removing the null check, the dereference needs to be flagged as critical, such that it cannot be removed by a subsequent pass.

Better still, if the compiler does try to remove something that was relied on by an earlier optimization step, alert the user that something is probably wrong with the code.

Maybe this is me coming from a C++ background rather than C, but that sounds like a horrible idea. That would mean that any code removed by a branch determination (e.g. if(0) in a macro/template expression) would be assumed to still exist for the further purpose of removing unused variables.

Also, this would prevent one of the main benefits of function inlining. Once you have inlined a function, the values of some of the parameters may be known at compile time, resulting in lots of those if(0) parameters. But that is something that was relied on in previous passes, even though it is known to be a static value in the current pass.

(comment deleted)
Why would it mean that? The compiler can still remove anything inside an if(0).
The reason for having UB in the first place is so compilers can exploit them so we can have more optimized programs.

UB is a feature, not a bug. So you don't really "clean" it up. Cleaning up of UB means a lot of overhead for programs.

Yes, but also, UB is a "feature" to work around the fact that C is a relatively un-expressive language with a simple type system. It's great that the compiler can optimize

    for (i = 0; i < *len; i++)
        array[i] = 0;
without having to check on each iteration whether len secretly points inside the array and got modified by the loop. It would be better if this were not a heuristic and there were clear compile-time information saying for sure that this optimization is safe.
If len and array are both of the same type then the compiler must assume they can alias (or one of them is char*). One has to explicitly state with restrict that they cannot alias.

In case of similar but where it’s float array[N] etc then compiler can assume that they do not alias.

Right, that's a heuristic, and the consequence of that heuristic is that a bunch of things where you intentionally have pointers of different types to the same memory - the sort of low-level data manipulation that C ought to be good for - is UB unless you are careful about it and you find one of the exceptions to this rule.
Not a heuristic. Well defined, even if it could be defined better. I'd personally make all types not alias by default and then have a separate keyword 'alias' to specify things that must alias.

One of the reasons why Fortran was faster for decades was the default aliasing rules, restrict finally got us out of that trap. If we'd always assume everything alias plenty of code would become way slower.

Also C has well defined ways to access memory via different types. Make an union type and access via that. It makes it explicit.

Is it really fair to call type punning "well defined"? To my knowledge, it was considered implementation defined from C89 all the way through C11.
Before C11 char* was the only way to do type punning, badly. So it was basically forbidden, but still well defined.

Since then they specified that unions are the way to go.

It's deterministic, but it's still a heuristic in the sense that it's a guess at the underlying intent of the code. You can come up with different, perhaps simpler guesses, but they're still guesses.

It's quite common in existing real-world C code to rely on the ability to fill in a variable via a pointer of one type and then access it via a pointer of another type, and you expect the language to properly return the data you wrote to that pointer. The C committee determined that, just about always, that happens when one of the pointers is a char pointer (e.g., you call read() or write() on a struct), and they said that char pointers are allowed to alias by default but others can't. That's their guess, which is mostly accurate. There are much nicer answers here if you're not obligated to be compatible with existing C code - but in that case, especially today, I'd go with one of several other languages that can accurately express intention at the source code level instead of making backwards-incompatible changes to C. :)

The reason was originally to admit portability to different architectures and different environments. The primary difference between undefined behavior and implementation-defined behavior is that implementation-defined behavior must be accepted by the compiler and yield a working program. Undefined behavior can error out at compile or run time -- but it doesn't have to.

Some examples:

1. When C came out, you couldn't assume a two's complement machine. So some architectures would yield different results on signed over/underflow than others, and some architectures might trap on overflow. Hence, signed overflow is undefined in C.

2. Did you know the Lisp Machine had a C compiler? It did. Because pointers on the Lisp machine were tagged and bounds-checked to ensure memory safety, this compiler implemented C pointers with fat pointers, containing a reference to an object and an offset within that object. Most machines you are familiar with allow indirecting through arbitrary integers; the Lisp Machine is an example of one that does not, trapping on invalid pointer accesses. Having C and C programs available in both environments mean that indirecting through a pointer that does not point to or inside some pre-existing static or dynamically allocated object is undefined.

3. Operating systems with virtual memory can arrange things such that indirecting through 0 (NULL) will raise a segmentation violation in user code, guaranteeing a pointer value that is always invalid -- but inside the kernel you may want to access memory location 0. So accessing null pointers is undefined.

By leaving these undefined, you are not committing implementors to adapt the abstract machine to the underlying hardware in cases where hardware can widely differ, allowing for simple implementations that yield fast code on all architectures they compile for yet still allowing for a useful subset of C code to be 100% portable.

You can force portability and remove UB at the same time. For example the system is not two's complement? just wrap everything math operation with checks on those system and say that your language is two's compliment!

Or just does what C does, make it UB and the programmers will be aware of implementation of overflows and won't suffer because of added checks. They will do the checks themselves if they want and handle their numbers with care

Imo the answer always boils down to performance. Java has no UB yet it works on billion devices (citation needed). It is doable, just costly.

C compilers use unsigned arithmetic even on hardware that offers built-in overflow traps for signed, two's complement arithmetic (no extra instructions required).
No, it doesn't boil down to just speed. The goal is close correspondence between C source and the emitted assembly. This gets you a few advantages, one of which is performance up to a point (beyond which you will need to optimize). Another is ease of implementation, on a variety of architectures.
Two's complement machines can trap on signed overflow, or set CPU flags that can be tested.

MIPS compilers for C emit "addu" instructions for signed integer additions rather than using "add", to avoid overflow traps.

Using unsigned arithmetic for signed is not just so that C programs relying on the behavior do not blow up. When overflow behavior is reversible wraparound, the compiler more freedoms in rearranging a calculation. Suppose the programmer has written some a + b + c, and ensured that there is no overflow (when it's added left to right, as the abstract semantics says). If the addition mechanism has reversible overflow, the compiler can rearrange it to do the additions in any order. The result will be the same, correct one that (a + b) + c would produce.

But the idea that two's complement itself "wraps around" is mostly a myth. Two's complement has a fixed range of representation, and a result of adding two elements from that representation can be out of that range. In that case, the truncated value whose bit pattern corresponds to the unsigned arithmetic is not the correct result.

The rationale does not mention optimization as a reason for undefined behaviour https://www.lysator.liu.se/c/rat/a.html#1-6
Eh, well but it is. Since most (all?) UB could be eliminated if you added more code. I don't see any other reason for stuff like making access to uninitialized memory UB for example.
(comment deleted)
What I mean with "pragmatic cleanup" is getting rid of obsolete assumptions about the underlying hardware. E.g. two's complement has won, there's no reason why this couldn't be reflected in the C standard by removing signed overflow from the list of undefined behaviour. I bet more such cases can be found (e.g. 8-bit bytes, NULL not being zero, etc...).

Even if such exotic hardware is still around, the only thing that would change is that C compilers supporting this hardware couldn't call themselves "ISO Compliant" anymore, the users of those compilers wouldn't lose anything.

C++20 is actually forcing two's complement I think. So it is happening slowly
The problem is that the initial concept was botched by accident (C89) and then embraced rather than corrected by later committees.

Nobody during C89 formulation realized that it would end up “a license for the compiler to undertake aggressive optimizations that are completely legal by the committee's rules, but make hash of apparently safe programs” that “negates every brave promise X3J11 ever made about codifying existing practices, preserving the existing body of code, and keeping (dare I say it?) `the spirit of C.'” If they had, the response would have been that “[UB] must go. This is non-negotiable.”

You have many quotation marks, but no reference to who said them. Those aren't quotes from the articles, so I can't find where they came from.
A letter to X3J11 from some chap named Dennis Ritchie.
For people who read the comments here but not the primary sources[0], you've put words into Dennis Ritchie's mouth. He did not say that UB must go, but that noalias must go. And, lo and behold, it did. So these (altered) quotes are not at all a fair characterization of what's actually in the standard.

[0]: https://www.lysator.liu.se/c/dmr-on-noalias.html

He said why ‘noalias must go’, and those reasons apply equally to the modern conception of UB, so it's clear that the consequences of the definition were not realized at the time.
With a nonoptimizing compiler the first example just segfaults at dereferencing P.

I’m not quite sure what the compiler is supposed to do in that case. Insert nullchecks by itself to avoid crashes?

Fun fact: if there were no C language definition, there would be no "undefined behavior."
In that case all C program behavior would be literally undefined.
Is that what you think of Python?

How can this be: C has a standard, when many other languages do not. Thus C is "more defined" than other languages. Yet somehow it ends up being less defined.

If C had no standard, it would be defined by its implementation, just like many other languages. But because it has a standard, it has undefined behavior.

The real trick is that without a standard, implementations have to be bug-compatible. They're too defined! The purpose of a standard is to define them less.

Python many times had the problem of being defined as CPython. Modules that target multiple Python implementations often have to work around differences between them, without a standard they have to look into implementation details of their targets. Not having a standard is definitely a pain.

But for most intents and purposes Python is defined by its documentation, and it's fine.

Having said that UB in C and C++ means behavior that the standard doesn't define. If you remove the standard then UB either loses meaning or it makes all programs undefined. It definitely doesn't make all programs defined.

If you remove the standard, wouldn't everything naturally be implementation-defined? Like, say, Python.
Yep and you would end up with what was occuring before there a standard was created (POSIX, etc) which is generally a giant dumpster fire of implementation defined hell.
Have you thought about why having a standard causes all these problems with undefined behavior? The point of a standard is to be less specific than a reference implementation. That's why it's written in English, not code. That means definitions are fuzzy, which is why every kind of English-language law, rule, or spec is vulnerable to malicious compliance.

If C had a judge, like the legal system, he could deny attempts to use fuzziness in the standard (intended to prevent specifying bugs) to introduce bugs. Unfortunately, C doesn't have a judge with common sense, it has people who think the standard is code and anything that is undefined really is a license to do whatever you want.

I guess this problem was inevitable as soon as the words "nasal demons" were put to keyboard. Now the only thing that can save us is a reference implementation.

Compilers use UB for optimization, because programmers de-facto demand it.

You'd hate a compiler where this has silly overhead:

    for(int i=0; i < n; i++) arr[i];
That's because 64-bit addressing modes don't overflow the same way as signed 32-bit ints. But signed overflow being UB gives compilers license to ignore the difference and generate code that makes sense.

The real problem is that C is too bare-bones and lax to be able to contain or avoid UB in day to day programs. For the aforementioned overflow gotcha, there's no way to enforce use of size_t for indexing. There's no `checked_mul(a,b)` to easily avoid signed UB. You have to make the check yourself, and if you're not careful, that check will be UB too. malloc() makes you multiply numbers often, exactly where it's dangerous. And on top of that implicit integer conversions can make unsigned arithmetic surprisingly signed.

Presumably you meant to do something with arr[i] or the whole loop could be elided. I'm not sure I understand your point, what is the type of n? If it's something bigger than int that's a bug and should result in a warning.
From what I understand, the type of n isn't really the important part here; it's the type of the indexing variable (i) that makes a difference. On platforms where sizeof(int) is not the size of a register (e.g., x86_64 with 32-bit ints), UB signed overflow allows the compiler to not have to emit code to deal with the possibility of 32-bit overflow, which probably results in "sensible" assembly.

This article [0] goes into a bit more detail.

[0]: https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...

The article you linked says that compilers can prove a lack of overflow in this example, so UB is not necessary to assume here. It also claims that the compiler cannot prove this in more complex examples (but gives no explicit examples). What I'm saying is if the compiler cannot prove a lack of overflow in this way, that should be a warning.
> The article you linked says that compilers can prove a lack of overflow in this example, so UB is not necessary to assume here.

It's trivial to prove in that particular example because it's assumed that count is also an int (both i and count are in 32-bit registers).

> It also claims that the compiler cannot prove this in more complex examples (but gives no explicit examples).

The example doesn't even have to be that complex; you just need sizeof(n) > sizeof(i) and i to be signed (godbolt example at [0]). In that example, if the compiler can assume signed overflow is UB, then you get fairly sensible code generation. If the compiler cannot make that assumption (add the -fwrapv flag), then you get that movsxd instruction in the middle of the loop body. If you use -O2 instead of -O1, then -fwrapv prevents loop unrolling.

> What I'm saying is if the compiler cannot prove a lack of overflow in this way, that should be a warning.

I'm curious how frequently this warning might fire in real-life codebases, or when it does fire how frequently it's something the programmer can actually fix. Clang does have -Rpass-missed to emit some diagnostics when an optimization cannot be performed, but it doesn't emit anything in this particular case (though I'm not sure whether it's supposed to).

[0]: https://godbolt.org/z/abq3n9

Something that I haven't seen discussed is why it wouldn't be a better solution would be for the code you listed to be a warnings or errors; get the user to write size_t then it is efficient without needing to go to UB. Another common example is a null-check after a deref; it seems like that should also just be a compiler error since the UB optimization is based on "you wrote code that the compiler can just directly tell is wrong, so it's going to pretend you didn't do that".

There's surely cases where it would be hard to suggest a "equally efficient without UB" replacement (eg when it wants to fuse two i64 into one), but it feels like the current position is too far on the axis of "provided with suspicious code, the compiler should just make that garbage run as fast as possible and call it a day"

And yet, they managed to write the Linux kernel in it, as well as thousands of other things we use every day.
Obviously the most dangerous form of undefined behavior is not that which always crashes but that which for some unlikely inputs cases crashes.
I'm not sure what your point is. The Linux kernel is riddled with undefined behavior and billions are spent on finding, removing, or mitigating that.
I wonder what would happen if i put hand into fire. But I have never done it in my life, because it might cause 'undefined behaviour'.

In my years of working with C its always the 'look how smart I am code' causes issues.

Keep it clean and simple. Avoid complexity and fluff features of C. And you will not going to have deal with exotic bugs.

I am pretty sure your hand going in fire is pretty defined behavior
Is it though, for yourself/your body. Is putting hand into a fire something you experienced and internalized?
Of all the possible things that could happen to me, I am very confident that only a small subset could result, so it is pretty well defined. Nasal demons, for instance, are not on the menu.
We are really straying off actual point.

Why would you not actively avoid undefined behaviour and instead take risk.

Zeroing out variables, checking pointers coming from external modules etc. Not rallying on a compiler to resolve fancy stuff.

Like the example:

void contains_null_check(int P) { int dead = P; if (P == 0) return; P = 4; }

Leave declarations uninitialized or zeroed Check input. Work with data.

void contains_null_check(int P) { int dead; if (P == 0) return; int dead = P //work P = 4; }

This leaves less space for error/interpretation and IMHO it keeps code cleaner.

Any fancy optimizations should be used only for critical path and only when absolutely necessary.

"Managed" is a strong word - there's regular complaining about compiler optimizations, there was a string of vulnerabilities where null checks were optimized out because they were secretly UB, and there was a discussion just last week about how compilers were reordering certain patterns in unexpected ways: https://linuxplumbersconf.org/event/7/contributions/821/

Quoting from the abstract of that talk: "... preserving source-level dependencies through to the generated CPU instructions is achieved through a delicate balance of volatile casts, magic compiler flags and sheer luck. Wouldn't it be nice if we could do better?"

One of the more pernicious problems is security checks written to detect underhanded violations of constraints being eliminated because the compiler doesn't recognize the underhandedness is possible.

In a past life, I depended on signed overflow to detect out of bounds memory allocations. That kind of stuff gets compiled away these days, you need to be cleverer to detect when the user is deliberately trying to invoke UB. And because the compiler doesn't believe in UB, there's the risk it's going to remove your detection logic.

Making signed overflow to be well defined would be so much better.

IIRC C++ just decided that signed numbers are two's complement and that's that. C could really use similar thing.

> IIRC C++ just decided that signed numbers are two's complement and that's that.

Signed integer overflow is still undefined in C++.

You are correct. I misremembered, it was just a proposal that was not taken into the spec.
No, it's taken into the spec. It affects unsigned <-> signed conversions and maybe other things. Not integer overflow though.
Ah. Makes sense as that’s relatively cheap to emulate by just doing conversion when loading and storing from memory, but allows the use of HW even in case of ones complement implementation.
Why oh why does "undefined behavior in C" come up constantly here? I wrote C every day for decades. I was aware there's such a thing as undefined behavior. I can't remember it ever being even a miniscule factor in my daily work. What's changed?
One piece of this is probably that compilers competing to generate the best code have increasingly incorporated understanding of undefined behavior into their optimizers, leading to things that were previously undefined but happened to behave in some particular way to instead behave in ways that break programs using undefined behavior.

Compilers incorporate inferences that can be made by assuming UB doesn't occur into their optimization because it improves code generation of correct programs.

This is one of the humps that C programmers eventually just need to get past: That UB means literally anything is possible[1]. Not just what you currently see happening with the current version of your compiler on your current platform. Anything! We've all run into a few programmers who would insist that their particular invocation of undefined behavior was fine, because as you said, it "behaves in some particular way". Now that we have more aggressively optimizing compilers we don't run into those folks so much anymore.

I wouldn't say it's a minuscule factor in my daily work, as OP put it, but it's not something to agonize about if you stay mindful of avoiding common undefined behavior pitfalls. It would be very helpful if compilers were better about warning you when you're venturing into UB, but that's a different topic.

1: You can even get the shirt: https://teespring.com/shop/undefined-behavior-shirt

>That UB means literally anything is possible

Many people say this but nobody "literally" believes it.

It's a way of expressing the righteous dominance of one person or group's attitude towards reasonableness over another's. In a way that forestalls argument.

"Literally anything" could literally mean global thermonuclear war.

Compilers started to generate surprising code more regularly. Here's an example of a function taking void* parameters, that are cast to uint64_t* and then used. Upto gcc 6.3, you get a "sensible" result. After gcc 7, with -O2, you get a surprising result. https://godbolt.org/z/ugSDmr

If you get Godbolt to run the program and show the output, you'll see it change with GCC version.

Obviously what is "sensible" and what is "surprising" is subjective.

People tell me that the problem with this code is that casting one pointer type to another and then dereferencing is undefined behaviour. The problem is that C code seems to do this all the time. For example, fread() in the standard library takes a void* as its first parameter. So at first glance, most code that uses fread() must exhibit undefined behaviour. eg this function:

    int foo(FILE *f) {
        int val;
        fread(&val, sizeof(val), 1, f);
        return val;
    }
Closer reading of the spec says something about it being fine to cast, say, an int* to a float* and then cast back before dereferencing it. In this example, we're OK, because we don't reference via the void* . But the fread implementation must do, right?

I'm left feeling like it is impossible to implement fread() in C and that the standard library's API is no longer considered a good example of how to construct an API.

I guess the original motivation for it being UB to cast is to do with alignment constraints on some hardware. For example, a machine might be able to read an int8_t from any alignment but might require 4-byte alignment for int32_t. If so, then casting a non-aligned int8_t* to int32_t* and deferencing it would indeed fail on that hardware.

But some would argue that it is not right that GCC 7 breaks such code on a machine where that problem doesn't exist.

> The problem is that C code seems to do this all the time.

People should write these bits in assembly (as little as possible, but doing the casting and things), and then keep their C code well-defined using these low-level library routines.

With this machine I propose, that requires 4-byte alignment for int32_t, then even if I wrote the code in assembly, I'd just get a unaligned access fault from the hardware. So my code still wouldn't work. I'd rather the spec was changed to reflect this reality. Instead of calling it undefined behaviour, it should say something more specific, like, "dereferencing a pointer that isn't correctly aligned may cause your program to abort".

The key difference is that the alignment (usually) cannot be determined at compile time, so the compiler would still have to generate code that worked if the alignment was correct at run time. I think this is what many people either expect or would like the compiler to do.

With that change to the spec, I could continue to write my code in portable C, and I'd only get an abort on the target with this alignment constraint.

> it should say something more specific, like, "dereferencing a pointer that isn't correctly aligned may cause your program to abort"

What if on some other machine it doesn't cause the program to abort but instead returns a broken result? How do you cover that in the spec?

> But some would argue that it is not right that GCC 7 breaks such code on a machine where that problem doesn't exist.

Right, so let's write these low-level things in C and not worry about the case of 'where the problem doesn't exist'.

> How do you cover that in the spec?

OK, how about, "dereferencing a pointer that isn't correctly aligned doesn't work on some hardware". I see that this is a hard area for the spec to do well. But the current situation isn't viable. I can't tell whether I can safely use fread for heavens sake.

I feel like dereferencing an incorrectly aligned pointer could be a runtime error, just like divide by zero or accessing memory that doesn't exist. Maybe that would be an improvement from the current status. Although I'm not sure the spec _could_ be changed like this at this point in time.

> I feel like dereferencing an incorrectly aligned pointer could be a runtime error

By checking the alignment on every access? That'd be far too expensive if the machine you're on doesn't trap.

No. Letting the hardware just do its thing. Like an out of bounds memory access or divide by zero.

Editing here because it looks like we've hit the max reply depth. This wouldn't be like UB in the same way that code that could generate an out-of-bounds memory accesses is not UB. The critical thing is that the compiler should assume that the hardware error does not occur.

But how would that be different to UB? If you let the hardware do whatever it wants that's no different to UB doing whatever it wants.
(comment deleted)
You could come up with hardware that does this for any aspect of the spec though. For instance, I could make an ISA whose ADD instruction was normal except that 2+2=5. Adding a check around each add instruction would certainly be very expensive. Does the existence of this mean that we should redefine addition (on _all_ hardware) to avoid the penalty? Would compilers for other architectures be justified in eliding `if(x+y==z) foo();` if it can prove that `x` and `y` are `2`?
> For instance, I could make an ISA whose ADD instruction was normal except that 2+2=5.

Yeah, but good luck selling that... for the reasons you've identified it'd be a nightmare to run C on it.

As long as fread internally writes into it as char* it’s fine. char* is the special case that aliases everything else. The ”Just dumb bytes” kind of pointer if you will.
You are correct. However, C programmers expect to be able to cast a void* to something other than a char* and use it. It is kind of necessary because the language doesn't support generics. I feel that the reasons for calling this UB are not as good as the reasons for making it valid code.
The reasons for that are performance. So that one doesn't have to assume everything aliases everything else. It could be more clear though, like having explicit alias qualifier which would make it alias all and by default nothing aliases, or vice versa. Right now there is no easy way to make two unrelated types alias, one has to go via char* or use unions.

You can cast void* to something else and it will work, but you can't expect to get a random void* and presume it has some valid well defined data for you to read. You must know where it came from and what's been done to it.

As a rule of thumb if you just write dumb bytes it's char*. If it's anything else you must know what you're doing and there are ways to perform type punning that's within the spec.

> The reasons for that are performance. So that one doesn't have to assume everything aliases everything else.

Can you expand on that? I don't understand.

Edit: I see the answer in one of your other comments:

> If len and array are both of the same type then the compiler must assume they can alias (or one of them is char*). One has to explicitly state with restrict that they cannot alias.

I'd happily take that hit. I can put restrict in the hot paths of my code. I'm tempted to say I want restrict to be the default, but I haven't thought that through.

You can use memcpy or unions. No C programmer that knows the language should be casting and dereferencing void* to anything other than char* for portable code.
> You can use memcpy or unions.

I know. I think the reason that this an area that causes lots of debate is because the majority of C programmers from 20 years ago would not have thought the memcpy or unions approach was a better idea than casting and dereferencing void* . The casting approach used to "just work" unless you violated the alignment requirements on your platform, and that was a problem 20 year-ago C programmers were happy to take on.

For next time this discussion comes up, I'll try to think of a good example of when the void* approach yields easier to maintain code :-)

BTW, you can also use -fno-strict-aliasing to make the C compiler sort-of work like it did 20 years ago. I believe this is what the Linux Kernel does but I couldn't be certain that it is for exactly this reason.

Well, it's not the act of casting/dereferencing void* that's UB (otherwise malloc would trigger UB) but the potential for violating alignment requirements.

If you're certain there is no violation (such as with malloc), then go ahead and do it. -fno-strict-aliasing is unrelated to this issue. It removes the strict aliasing assumption from the compiler (another source of potential issues if not understood), but void* (and char*) are explicitly allowed to alias to anything regardless.

I don't understand. Compiler experts have told me that the Godbolt example I posted at the start of the thread (https://godbolt.org/z/ugSDmr) is incorrect C code because casting void* to uint64_t* is UB.

I modified the code to ensure that the data is 8-byte aligned (I think I've done this correctly). It still goes "wrong". https://godbolt.org/z/Pnxnb8

In either case, adding -fno-strict-aliasing fixes it.

Casting void* to uint64_t* is definitely NOT UB, as I said this would break:

    uint64_t *a =  malloc(8);
What you're running into in the context of your example, is breaking strict aliasing.

You're starting with uint32_t pointers, which then go through void pointers and end up being accessed through pointers to uint64_t. This breaks strict aliasing since void* can not be used to bypass it.

If you had started with uint64_t, you could go through void* and back to uint64_t without any issues.

OK. That does match what the compiler experts told me. So is the rule that it is OK to cast a pointer back to its "original" type and dereference it? If so, I feel this rule breaks the abstraction mechanisms C provides. eg now when I'm implementing that xor512b() function, I need to know about the invisible "original" type of the arguments.

And in the case of malloc, I don't know what type the data was "originally" before it returned it to me. I guess I have to treat malloc as a special case where I can cast its void* to anything safely. That would seem morally wrong.

Strict aliasing violations happen when values are accessed, not during casting.

You need to have a model of how the compiler/optimizer works. In your example, it's important to know what objects you're working with and what the effective types of these objects are. Focus on the objects, not on the pointer types that you use to access them. Normally, the effective type of an object is its declared type. Allocated objects however, have no declared type according to the standard [1]. They get their effective type on first access:

"If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value."

This means that you don't care how malloc works internally (unless you're writing one), and casting/accessing its result - which is guaranteed to be properly aligned - does not break strict aliasing.

But in your example, the strict aliasing violations are crystal clear. You declare uint32_t objects, therefore the compiler knows and can track the effective types of these objects. Later, you're trying to access the values of these objects through different types which is when the strict aliasing violation occurs.

> So is the rule that it is OK to cast a pointer back to its "original" type and dereference it?

Since you're accessing the same object and there's no mismatch between its effective type and the type you're using, you're ok.

> If so, I feel this rule breaks the abstraction mechanisms C provides. eg now when I'm implementing that xor512b() function, I need to know about the invisible "original" type of the arguments.

void* is not really an abstraction and should not be used as such (or to avoid thinking about designing a proper interface). You can make use of the type system by declaring proper types and being consistent, or you can use char* as a byte-level interface. Usually, when you see void* in an interface, it's clear what the implications are and how you should proceed with it. If it's not, you're looking at bad design.

[1] http://www.iso-9899.info/n1570.html Footnote 87) Allocated objects have no declared type.

There is no undefined behavior in your example period.

The fread implementation sees dest as a void pointer and will access it going through char*, which is explicitly allowed.

UB is blown out of proportion by compiler vendors and language-lawyer types concerned with portability across exotic ranges of machines, including those, where, for example, CHAR_BIT != 8.

For the most part, avoiding UB is common sense when you grasp the basic abstract C memory model - types of storage and their lifetime, avoiding out of bounds array access, keeping track of the lifetime of all allocations, etc.

That's not the issue. The issue that simple bugs become global program transformations into nonsense due to aggressive optimizations, when they should instead be compile-time errors.
It's actually a big problem and anything but common sense.

Do all C programmers know what strict aliasing means?

How about pointer provenance?

Have they memorized integer promotion rules?

These are all potential sources of undefined behavior that can lead to catastrophic results.

I bet most experienced C programmers with decades under their belt would fail one or more of these tests.

Other folks have mentioned that optimizing compilers are more aggressive these days. To that I'd add:

- More applications are talking to the internet and being exposed to malicious input. There's also more money to be made in finding exploits, and more people and governments looking. At the same time, fuzzing techniques are getting dramatically better.

- Applications are more likely to run across different architectures (mobile) and different compiler versions (faster pace of compiler development), leading to more opportunities for "something weird" to happen. Multithreading is also more common.

- Memory-safe languages have taken a larger market share, and the popular attitude towards memory safety issues is changing from "bugs are a fact of life" to "this is a drawback of specific languages".

How exactly do you know it wasn't a factor? Do you have an infallible method of identifying UB in C?
The example case is flagged by a number of static checkers, for example Coverity, as a likely error (pointer is dereferenced before it is tested for null).
Why the title change? The original title "Why undefined behavior is often a scary and terrible thing for C programmers" may be a bit polemic but occurs verbatim in the article - and IMO does a better job of expressing the article's message.