only until you get deeper into how the hardware actually work (and OS to some degree)
and realize sometimes the UB is even in the hardware registers
and that the same logical memory address might have 5 different values in hardware at the same time without you having a bug
and other fun like that
so the insanity is reality not the compiler
(through IMHO in C and especially C++ the insanity is how easily you might accidentally run into UB without doing any fancy trickery but just dumb not hot every day code)
None of what you said makes any sense. You're mixing "UB" and bugs.
UB is about declaring programs invalid with a snide "don't do that", not about incorrect execution due to an incorrect specification. E.g. speculative execution running in privileged mode due to a prior syscall is just a plain hardware bug. It's not undefined behavior. In fact, the bug in question is extremely well defined.
The closest thing to reading undefined behavior is reading a "don't care" or VHDL's 'U' value from a std_ulogic and even those are well defined in simulation, just not in physical hardware, but even there they can only ever be as bad as reading a garbage value. Since a lot of the hardware design is non-programmable, there is also usually no way to exploit it.
There is no UB in hardware registers or physical DRAM, I don't think you actually have familiarity with how the hardware works if you make this claim. (Or perhaps you aren't familiar with how crazy "UB" in the sense of the ISO C documentation is)
EDIT: one could see "apparent" violation of memory consistency if say the cache subsystem or memory controller were misconfigured, however this would require both (1) you are running in kernel mode, not user-space (2) you have a bug, so GP's claim is not supported that bug-free code could encounter such a state.
> There is no UB in hardware registers or physical DRAM
This seems very sensitive to specific definitions that others might not share. DRAM is provided with a spec sheet that defines its behavior (if you write to an address, you’ll read back the same value from the same address in the future) under certain conditions. If you violate those conditions, the behavior is… undefined. If you operate DRAM with the wrong refresh timing, or temperature, or voltage, or ionizing radiation level, you may see strange behavior. Even non-local behavior, where the value read from one cell depends on other cells (RowHammer). How is this not UB?
If a C program accesses uninitialized memory (UB), it is perfectly compliant with the language spec for the compiler to reformat your hard drive and replace your OS code with crypto mining.
I'm not exaggerating by a legalistic interpretation, and I'm only slightly exaggerating in practice. UB can do some really weird, unintuitive stuff in practice on real hardware:
The point is that this extreme UB should never happen. It was a choice of the compiler implementors, and rather than fix this, they allowed the escape hatch of UB in the spec. It would be more sensible for the compiler to say that, e.g., accessing uninitialized memory results in a nonspecified value, or even possibly multiple different nonspecified values if accessed from different threads. That captures what we expect to happen, but would be (according to C language spec lawyers) defined behavior.
In practice, it would mean that compliant compilers will ensure that any situation in which uninitialized memory could be accessed would not result in weird edge case page faults on certain architectures or whatever that could in fact lead to wacky UB situations.
But isn't this exactly parallel to the Rowhammer case in DRAM? When operating at the edge of the spec, the behavior of DRAM becomes undefined. (And, of course, one challenge with Rowhammer was about /which/ edge of the spec this happened on.) In this case, writing one physical address altered the contents of other physical addresses. This is "really weird, unintuitive stuff … on real hardware." And of course we can (and do) ask DRAM vendors to not take advantage of this undefined behavior; but they do so as an optimization, allowing slightly smaller and more closely spaced DRAM cells, and thus higher density DRAM dice for the same price. Just like it's possible to work with a language with fully-defined semantics at the cost of performance, it's possible to buy DRAM with much wider specifications and more clearly defined behavior up to the edges of those specifications… at the cost of performance.
Extreme UB in both hardware and software is a choice of priorities. You may favor giving up performance capabilities to achieve a more understandable system (so do I! I do most of my work on in-order lockstep processors with on-die ECC SRAM to maximize understandability of failure modes), but the market as a whole clearly does not, in both hardware and software.
Legitimate bugs in hardware is probably out of scope for compilers. That would be an unreasonable ask, yes. But I believe it should be the job of the compiler to make sure that a correctly executed program on a reference machine strictly adhering to the architecture specs should not result in UB.
I'm not saying that a computer architecture should be UB-free. That would be awesome if it could be done, but in practice probably a bridge too far. But a compiler should map high-level directives into low-level implementations on a specific architecture using constructs that do not result in UB. This is not too much to ask.
A compiler can't reasonably protect you from rowhammer attacks. But it should guarantee that, barring hardware errors, accessing uninitialized memory has no effect other than something sensible like returning unspecified contents, or causing a memory access exception, or whatever. It should be defined up front what the behavior is, even if some of the runtime values are unpredictable.
As a more concrete example, most languages these days clearly define what happens in signed integer overflow: the thing that you expect to happen in any two's complement machine (char)127 + (char)1 == -128. C treats this as undefined behavior, and as mentioned in my link above that can cause what should be a finite loop (with or without overflow) to compile as an infinite loop. This "optimization" step by the compiler should never have happened. C resists changing this because C compilers exist for non-twos-complement architectures where the behavior would be different. IMHO the correct approach would be to require THOSE weird esoteric architectures to compile in extra overflow checks (possibly disabled with opt-in compiler flags that explicitly violate the standard), rather than burden every C developer everywhere with the mess that is signed arithmetic overflow UB.
It's a matter of expectations. Any but the most junior programmers expect that signed integer overflow will not be portable. That's fine. But they expect a sensible result (e.g. wrap around on two's complement machines), even if it is non-portable. They don't expect the compiler to silently and sneakily change the logic of their program to something fundamentally different, because UB means the compiler can do whatever tf it wants.
> Legitimate bugs in hardware is probably out of scope for compilers.
But that's exactly the point. The "bug" of RowHammer was that it occurred slightly on the "allowed" side of the envelope, at acceptably-low refresh rates. The "UB" of RowHammer and a hundred other observable effects is that, on the "disallowed" side of the envelope, the behavior is undefined. The system designer gets to choose at what probability they are on each side of the envelope, and the trade-offs are very much optimization opportunities.
Writing software in C that may exhibit undefined behavior is exactly this -- it's choosing, as a software engineer, to be on the far side of the specification envelope. In exchange, you get access to several powerful optimizations, some at the compiler level, and some at the career level (if you think that not needing to learn to use understand your language properly is at time optimization, at least).
I've edited my claim to be a bit more clear, however in the context of parent's claim we are talking about bug-free code on an non-buggy physical processor, and I think implicitly we are talking about user-mode code where one does not have the ability to alter any DRAM timing configuration registers anyway.
In the ARM documentation this is referred to as “UNPREDICTABLE”. The outcome is not defined. It may work. It may not. It may put garbage data in a register.
I've edited/clarified the claim above. Parent did say "without you having a bug" and entering an UNPREDICTABLE state would be buggy code. Also, "maybe puts garbage data in a register" is not UB, it's a much more reasonable thing in UB (the definition of UNPREDICTABLE prior to ARMv8 does seem to allow for UB, however). I don't believe that such an UNPREDICTABLE state is reachable from user-space/unprivileged code (or else it would violate the chip's security properties) -- but if I'm wrong on that I'd be interested in an example.
I do not find it so easy to accidentally run into UB in C if you follow some basic rules. The exceptions are null pointer dereferences, out-of-bounds accesses for arrays, and signed overflow, all those can be turned into run-time traps. The rules include no pointer arithmetic, no type casts, and having some ownership strategy. None of those is difficult to implement and where exceptions are made, one should treat it carefully similar to using "unsafe" in Rust.
and realize sometimes the UB is even in the hardware registers
and that the same logical memory address might have 5 different values in hardware at the same time without you having a bug
and other fun like that
so the insanity is reality not the compiler
(through IMHO in C and especially C++ the insanity is how easily you might accidentally run into UB without doing any fancy trickery but just dumb not hot every day code)