 No outcome is a bug if caused by UB. For example: signed integer overflow in C, or using
unsafeto create two
&mutreferences to the same data in Rust. Wikipedia has an excellent list of examples if you'd like to see more.
Here's the list of guarantees compilers make about the outcomes of undefined behavior:
That's the whole list. No, I didn't forget any items. Yes, seriously.
It is possible to analyze how UB affects a specific program when compiled by a specific compiler or executed on a specific target platform. For example, there exist exotic compilers, operating systems, and hardware that offer additional guarantees Like CHERI, with awesome powers around pointer safety. relative to most common platforms, which only guarantee OS-level process isolation. We aren't talking about those in this post.
The mindset for this post is this: "If my program contains UB, and the compiler produced a binary that does X, is that a compiler bug?"
It's not a compiler bug.
All of the following assumptions are wrong
Falsehoods about when UB "happens"
- Undefined behavior only "happens" at high optimization levels like
- If I turn off optimizations with a flag like
-O0, then there's no UB.
- If I include debug symbols in the build, there's no UB.
- If I run the program under a debugger, there's no UB.
- Okay there's still UB with all of these, but my code will "do the right thing" regardless.
- It will either "do the right thing" or crash with a
- It will either "do the right thing" or crash somehow.
- It will either "do the right thing" or crash or infinite-loop or deadlock.
- At least it won't run some unrelated code from elsewhere in the program.
- At least it won't run any unreachable code the program might contain.
Falsehoods around the behavior of executing UB
- If a line with UB previously "did the right thing," then it will continue to "do the right thing" the next time we run the program.
- The UB line will at least continue to "do the right thing" while the program is still running.
- It's possible to determine if a previous line was UB and prevent it from causing problems.
- At least the impact of the UB is limited to code which uses values produced from the UB.
- At least the impact of the UB is limited to code which is in the same compilation unit as the line with UB.
- Okay, but at least the impact of the UB is limited to code which runs after the line with UB.
UB is explicitly allowed to alter the behavior of other code, even including operations preceding it! "Alter" here encompasses corrupting, undoing, or altogether preventing (as if it never happened) the outcomes of that other code. To learn more and see examples of UB causing "time travel," check out this blog post.
Thanks to these two Reddit posts for suggesting better wording for these items. For the original text, see the Errata section at the end of this post.
Falsehoods about the possible outcomes of UB
- At least it won't corrupt the memory of the program.
- At least it won't corrupt the memory of the program other than where the UB-affected data was located.
- At least it won't corrupt the heap.
- At least it won't corrupt the stack.
- At least it won't corrupt the current stack frame. (My name for this is the "local variables are safely in registers" fallacy.)
- At least it won't corrupt the stack pointer.
- At least it won't corrupt the CPU flags register / any other CPU state.
- At least it won't corrupt the executable memory of the program. OS and hardware security features like W^X can make this unlikely, but self-modifying programs can be built so it's in principle possible through UB as well. Certainly there's no guarantee that UB won't do this!
- At least it won't corrupt streams like stdout or stderr.
- At least it won't overwrite any files the program already had open.
- At least it won't open new files and overwrite them.
- At least it won't completely wipe the drive.
- At least it won't damage or destroy any hardware components. Not all devices have the same level of self-protection against bad inputs written to their control registers. This is the k