im not an c++ expert, but it looks like the unreachable macro of rust.
In rust it allows optimizing the generated binary to not care about special paths.
If an integer can only be 1-4, then it doesn't make sense to dedicate an code path for 0 or 5+ that will never be called.
in an explicit for loop its easy to know for the compiler, but when this information crosses boundaries like function calls then it can make it easier and can reduce the binary size.
To elaborate on the other reply to your comment: In Rust, if you don't use `unsafe` and still manage to trigger UB, then that's a bug in the compiler or the library code you called (which in turn has to have used unsafe somewhere to trigger UB).
So the fact that `unreachable!()` does not require the use of the keyword `unsafe` means that it can't be like C++ `std::unreachable()`.
The solution is to avoid UB. std::unreachable() is just an easy-to-invoke way of hitting UB, mentioned for the sake of the discussion. They did later take your advice “Let’s say we change std::unreachable() to int* ptr = nullptr; *ptr = 99; in the previous example.”. They still got improper codegen out of the compiler.
Though, it would be better if programmers read deliberate instances of UB as if they literally said "std::unreachable()" at that point in the code, as that would better convey how the optimizer passes will see things after dozens of frontend and middle-end passes have run before it.
> There's no standard terminology I'm aware of to distinguish the two.
There is: implementation-defined behavior. Your example of writing to an MMIO address is such an example, as the standard says casting an int to a pointer is implementation-defined. (However, the C standard has plenty of cases of undefined behavior that probably should have been implementation-defined.)
You seem to be conflating the act of writing to an arbitrary address with the act of assigning that arbitrary address to a pointer to begin with. They are distinct actions.
> There's no standard terminology I'm aware of to distinguish the two. But the distinction exists.
This distinction exists, but not to the compiler. And the compiler is the thing building your binary. You can complain if you like about how UB that could potentially instead have been implementation-defined is UB instead.
But until then, the compiler will mangle your code either way, whether you feel it is fair or not.
No. That's just one of the MANY ways to trigger UB. In the article I also trigger UB by dereferencing a null pointer instead.
In fact, here's an example where calling `unreachable()` and dereferencing a null pointer produce the EXACT same assembly: https://godbolt.org/z/538qfj9z9
This is sort of true, except that behavior can be undefined in the standard but defined in your implementation. One of the best examples is -fwrapv, which makes signed integer overflow not UB. And Fil-C has no undefined behavior that I'm aware of. But most implementations target the standard and "define" what the standard doesn't to be "whatever I feel like, in service of implementation simplicity and performance". What matters is what your implementation guarantees, which should be a superset of what the standard does.
Fil-C does still have UB. Signed-overflow, for instance, is still UB in Fil-C, and can still lead to things like the optimizer removing checks in the presence of UB.
It does not permit taking over pointers and doing a plain old RCE, sure, but it still does permit a variety of optimizations that could break the logic of your program in surprising ways.
Here are two examples of where it's clear that the UB from the underlying C is still present and the optimization passes are still able to do things that wouldn't be possible without UB, like falling through functions and deleting safety checks inserted by the Fil-C runtime:
I'm not really convinced you can take LLVM and just flip a few poison bits off and remove a couple optimization passes and know that UB isn't going to explode in your face.
I stand corrected! I had assumed that Fil-C actually removed poison and unreachable states in the IR but thinking about it more that seems very challenging to do comprehensively.
The first example should have changed the heading to "C++ code" as opposed to "C code" due to the use of `std::unreachable()`. This matches the godbolt link.
Another alternative is to use `__builtin_unreachable()` (not standard, but available in GCC and Clang).
C23 also has the "no time travel" stipulation. I expect C23 should be immune to the example 1 program issue, because of that. A quick trial on godbolt (https://godbolt.org/z/8K81PYoYE) seems to agree, with the caveat that I didn't work out the right compiler options to show for sure.
C++26, IIRC, adds "no time travel" to C++. But note that your example still creates exploding chocolate, so what do you mean by "immune"? C compiler with -std=c23 -O1 produces literally the exact same assembly output.
Of course things go badly once we reach "unreachable()", continuing into the next function because the compiler didn't bother to add a "ret". That's no surprise, "unreachable()" is basically an instruction to the compiler to stop generating code from that point on, and the compiler obeyed.
Before that the observable side-effect, device_command=42, has already happened. With time travel allowed, could also have been omitted, which would have been a real problem.
I misread the article to think that is was about the latter problem. You are right, my mistake. I didn't consider that anyone could see the former as a problem at all: It's just the compiler doing exactly what it's told to do.
The effect of UB is arbitrary and can change between compilers, between versions of the same compiler, between optimization levels of the same compiler version, between different platforms targeted by the same compiler, etc. It also can change depending on what other code is in the same compilation unit (which, with LTO, means any other code anywhere in the program can affect the behavior)
When you say "this UB does ... (like generates a trap instruction)" you are being hyper specific for a given compiler version and platform and optimization level and any other knob. It may not stay the same after you update the compiler. And what happens a lot is that newer, better optimizations (such as more opportunity to autovectorization), which attract people to newer compiler versions, may exploit UB in more creative ways.
For example, to exploit autovectorization, compilers may want to push some test out of a loop. If this test contains UB, this optimization will change where the UB is triggered. So that trap instruction may happen at a different place in the code, depending on optimizations.
> "the definition of std::unreachable is: invokes undefined behaviour" does not mean that all undefined behaviour is equivalent to calling std::unreachable.
Well, obviously not. The definition of UB is that anything can happen, so by transitive property it means that std::unreachable can trigger any behavior.
That's the whole point of UB, and the point of my post: It's UB! You cannot reason about what will happen! The whole point of the post is "No you CANNOT reason about what will happen". So of course I am making no claims about what will happen, just three interesting observations about what did in fact happen when I happened to try it, to prove this point.
31 comments
[ 0.40 ms ] story [ 29.0 ms ] thread[1] https://doc.rust-lang.org/std/macro.unreachable.html [2] https://doc.rust-lang.org/std/hint/fn.unreachable_unchecked....
So the fact that `unreachable!()` does not require the use of the keyword `unsafe` means that it can't be like C++ `std::unreachable()`.
Though, it would be better if programmers read deliberate instances of UB as if they literally said "std::unreachable()" at that point in the code, as that would better convey how the optimizer passes will see things after dozens of frontend and middle-end passes have run before it.
There is: implementation-defined behavior. Your example of writing to an MMIO address is such an example, as the standard says casting an int to a pointer is implementation-defined. (However, the C standard has plenty of cases of undefined behavior that probably should have been implementation-defined.)
This distinction exists, but not to the compiler. And the compiler is the thing building your binary. You can complain if you like about how UB that could potentially instead have been implementation-defined is UB instead.
But until then, the compiler will mangle your code either way, whether you feel it is fair or not.
No. That's just one of the MANY ways to trigger UB. In the article I also trigger UB by dereferencing a null pointer instead.
In fact, here's an example where calling `unreachable()` and dereferencing a null pointer produce the EXACT same assembly: https://godbolt.org/z/538qfj9z9
Even if that's the case, it's not "undefined" in the sense that it doesn't permit "now your program is the attacker's program".
Here are two examples of where it's clear that the UB from the underlying C is still present and the optimization passes are still able to do things that wouldn't be possible without UB, like falling through functions and deleting safety checks inserted by the Fil-C runtime:
https://news.ycombinator.com/item?id=49677463 https://news.ycombinator.com/item?id=49681273
I'm not really convinced you can take LLVM and just flip a few poison bits off and remove a couple optimization passes and know that UB isn't going to explode in your face.
But then it's not UB, and not the topic at hand. So I wouldn't call it "sort of true" for a statement about UB, because it doesn't apply to not-UB.
> But most implementations target the standard and "define" what the standard doesn't
Well, it's also different in a consteval context. But that doesn't make it less true outside consteval context.
Another alternative is to use `__builtin_unreachable()` (not standard, but available in GCC and Clang).
Before that the observable side-effect, device_command=42, has already happened. With time travel allowed, could also have been omitted, which would have been a real problem.
I misread the article to think that is was about the latter problem. You are right, my mistake. I didn't consider that anyone could see the former as a problem at all: It's just the compiler doing exactly what it's told to do.
Now if this actually made it into the C++26 standard, I've not paid to get the official one, but it's in the N5050 draft: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/n50...
Yes, into the shame cube I go. It doesn't change anything, and it applies equally to C and C++, but you are correct.
When you say "this UB does ... (like generates a trap instruction)" you are being hyper specific for a given compiler version and platform and optimization level and any other knob. It may not stay the same after you update the compiler. And what happens a lot is that newer, better optimizations (such as more opportunity to autovectorization), which attract people to newer compiler versions, may exploit UB in more creative ways.
For example, to exploit autovectorization, compilers may want to push some test out of a loop. If this test contains UB, this optimization will change where the UB is triggered. So that trap instruction may happen at a different place in the code, depending on optimizations.
> "the definition of std::unreachable is: invokes undefined behaviour" does not mean that all undefined behaviour is equivalent to calling std::unreachable.
Well, obviously not. The definition of UB is that anything can happen, so by transitive property it means that std::unreachable can trigger any behavior.
That's the whole point of UB, and the point of my post: It's UB! You cannot reason about what will happen! The whole point of the post is "No you CANNOT reason about what will happen". So of course I am making no claims about what will happen, just three interesting observations about what did in fact happen when I happened to try it, to prove this point.