They are in the process of rewording such cases as erroneous instead of UB, but it will take time.
If it's simply erroneous, then it behaves like in every other language outside C and C++: it leaves memory in a bad state if it happens at runtime, but it doesn't have any effect at compile time.
For example, if you do an out of bounds write in C, you can set some part of an object to a value it never should have according to the text of the program, simply because that object happened to be placed in memory next to the array that you wrote past the end of.
According to the semantics of the C abstract machine, the value of a non-volatile object can only change when it is written to. But in a real program, writes to arbitrary memory (which are not valid programs in C's formal semantics) can also modify C objects, which would be called a "bad state".
For example, take this program, and assume it is compiled exactly as written, with no optimizations at all:
void foo() {
int x = 10;
char y[3];
y[4] = 1;
printf("x = %d", x);
}
In principle, this program should print "10". But, the OOB write to y[4] might overwrite one byte of x with the value 1, leading to the program possibly printing 1, or printing 266 (0x010A), or 16777226 (0x0100000A), depending on how many bytes an int has and how they are laid out in memory. Even worse, the OOB write may replace a byte from the return address instead, causing the program to jump to a random address in memory when hitting the end of the function. Either way, your program's memory is in a bad state after that instruction runs.If you want an example of something that is not UB leaving memory in a bad state, here is some Go code:
global := 7;
func main () {
go func() {
global = 1000000;
}()
go func() {
global = 10
}()
fmt.Printf("gloabl is now %d")
}
The two concurrent writes may partially overlap, and global may have a value that is neither 7 nor 10 nor 1000000. The program's memory is in a bad state, but none of this is UB in the C or C++ sense. In particular, the Go compiler is not free to compile this program into something entirely different.Edit: I should also note that a similar C program using pthreads or win32 threading for concurrent access is also an example of a program which will go into a bad state, but that is not UB per the C standard (since the C standard has no notion of multithreading).
You also have languages using immutable or persistent data structures in their std lib to side-step the problem.
Because, me despite knowing both since the 1990's, I rather leave that to static analysis tools.
Making sure you have no UB certainly slows you down considerably, and I strongly prefer languages that can catch all non-defined behavior statically for sure, but I don’t find C to be unmanageable.
Memory alignment issues only happen when you cast pointers from the middle of raw memory to/from other types, which, yes, is dangerous territory, and you have to know what you are doing there.
It would help if there was better tooling for finding places that could result in UB.
[1]: although some of them can be a little surprising, like the fact that overflow is defined for unsigned types but not signed types
Even today you may find issues with signed integers. The ESP32 vector extensions seem to saturate signed integer overflow [1].
[1]: https://bitbanksoftware.blogspot.com/2024/01/surprise-esp32-...
But the amount of actual problems I have with UB in typical projects is very low by just some common sense and good use of tools: continuous integration with sanitizers, using pointers to arrays instead of raw pointers (where a sanitizer then does bounds checks), avoiding open coded string and buffer operations, also abstracting away other complicated data structures behind safe interfaces, and following a clear policy about memory ownership.
For every C operation you type, ask yourself what is its "contract", that is, what are the preconditions that the language or the library function expects the programmer to ensure in order for their behavior to be well-defined, and do you ensure them at the particular usage point? Also, what are the failure modes within the defined behavior (which result in values or states that may lead to precondition violations in subsequent operations)? This contractual thinking is key to correctness in programs in general, not just in C. The consequences of incorrect code are just less predictable in C.
Are about C++. There's an order of magnitude difference in the cognitive level to visually spot UB in C code vs visually spotting UB in C++ code.
As I said, Google does not prioritize for technical expertise, primarily because that's quite individual-centric. They're a large organization and their goal is to make sure people are formatted and replaceable.
They hire generally smart people and mold them to solve problems with the frameworks that they've previously built, with guidelines that are set such that anyone can come and pick it up and contribute to it, in order to facilitate maintenance in case of turnover or changing teams.
They also hire a lot of people straight out of university, with many people spending large portions of their career there without seeing much of the outside world.
As a result, their workforce is not particularly adept about using third-party tools in a variety of situations; they're optimized to know how things work at Google, which is its own sub- (or arguably mono-)culture.
Being an expert requires using a tool in many diverse codebases for widely different applications and domains. A large organization like this is not a particularly good data point to assess whether people can become good experts knowledgeable about the gotchas of a programming language.
Why else would corporations be the exclusive authors of "middle-level" languages like Java, C#, and Go? ;p JS and Python are too slow, but we can't find enough C, C++, or Rust developers! Let's invent these abominations instead with which we can churn out more good-enough mediocrity per quarter than ever before!
Maybe you can argue that C doesn't have a “ridiculous amount” of UB (even though the number is large), but C++ is so much worse I don't think saying it's “ridiculous” is off the mark.
And not only the amount is already ridiculous but every new feature introduced in modern versions of C++ adds its own brand new UB!
IIRC the overflow in SHA3's reference implementation was hard to catch also for ststic analisys tools, and had the practical impact of making it easy to generate collisions.
You're basically saying that every project in the wild has bad “coding strategy”…
> I expect that we will have full bounds safety options in compilers soon
Which will be disabled in most places because of the overhead it incurs.
> But having a good ownership model and good abstractions also avoids most problems here in my experience. I rarely have actual problems in my projects related to this.
It's easier said than done when you have no way of enforcing the ownership, and practically intractable when not working alone on a codebase.
It's not about crash at all, but “all software has security vulnerabilities because of UB” is unfortunately true.
> It is not intractable, in my experience you just have to document some rules and occasionally make sure they are followed.
If even DJB couldn't get that part perfectly I'm pretty certain you cannot either.
You're right, only software that has actual users cares about security.
> I run a lot of large simulations. I do not care at all if that software would crash on specially prepared inputs.
But it's not the 50s anymore and digital simulation is a tiny fraction of the code ever written nowadays so it's not a very good argument.
> I do care that it's as fast as possible.
You don't realize it but it ruins your entire argument. If speed is all that matters for your use-case then:
- there's no way you can use runtime bound-checks, and you unconditionally need the compiler to optimize as much as possible around UB, even if it breaks your program every once in a while.
- you likely can't afford dynamic memory allocation, which makes the UAF/double free kind of bugs irrelevant. Not out of “good coding strategy” but because you never free in the first place…
These are hypothesis that don't apply to software industry at large.
2. Sandbox escapes are commonplace, and not everything can even be sandboxed at all.