4 ms·
As an engineer, my job isn't to write code, it's to deliver systems that do specific things. That means that I need to understand the defined behavior of the co
by AlotOfReading 3y ago
As an engineer, my job isn't to write code, it's to deliver systems that do specific things. That means that I need to understand the defined behavior of the code I put into the system. Undefined behavior anywhere means you lack defined behavior everywhere in C/C++.
You can't work around this by writing more code or eliminate undefined behavior with tools like linters and tests. Your one and only option is to write perfect code that only has defined behavior. The number of people that can accomplish this in practice rounds to zero.
So yeah, how can you not care about UB? It's the semantic elephant in the room. Every conversation has to include it, implicitly or not.
- charcircuit 3y agoBecause behavior does eventually get defined somewhere. Just because it's not defined in the C standard it does not mean you can't reason about it.
- AlotOfReading 3y agoNo, if it was defined somewhere, it'd have a consistent behavior and it wouldn't "time-travel" the way UB can. The word for this in the standards is unspecified behavior. Undefined behavior doesn't need to have any requirements. Different parts of the toolchain and runtime environment (or even different compiler passes) may assume different behaviors for the construct. Even different calls to the same function with the same arguments may produce different behaviors. Let's walk through a simple example to make this clear. Let's assume you have a macro function foo() that triggers some trivial UB, perhaps integer overflow. Let's also say that this macro function is called the same way in two different translation units. Because there are no requirements on UB by definition, there's no guarantee that those calls will do the same thing, even on the same runtime, using the same compiler, with the same flags. Even the same line of code calling the same arguments may see different things every time, because again there are no required behaviors. Even code that does not itself trigger UB, but is on an execution path with UB does not have a defined behavior and will commonly be omitted by optimizing compilers like GCC. This has resulted in Linux vulnerabilities where null pointer checks were omitted from the actual binary because other code was "proven" by the compiler to dereference the pointer first.
- charcircuit 3y ago>Because there are no requirements on UB by definition, there's no guarantee that those calls will do the same thing, even on the same runtime, using the same compiler, with the same flags. Reread my comment. You are talking about behavior not defined by the C standard which I addressed in that comment. Compilers are deterministic. Reproducible builds are a thing.
- AlotOfReading 3y agoReproducibility is an entirely unrelated issue. The same compiler can produce different assembly for the same code depending on the surrounding context, or any number of other reasons. A reproducible build just means that you'll get the same binary each time you build it. Furthermore, the same generated assembly can produce different results each time it's run, as data races do. In that case, the only "definition" comes down to the essentially unknowable physical state of the system.
- charcircuit 3y ago>Reproducibility is an entirely unrelated issue. No, reproducibility is about having a defined output for a given source code and toolchain.
- AlotOfReading 3y agoI wrote up a quick example demonstrating UB compiling to two different implementations: https://godbolt.org/z/nd7GrP44s https://godbolt.org/z/nd7GrP44s Ignore how silly the actual code is and notice that the -O0 assembly checks the pointers before dereferencing them while the -O2 assembly does not. Same compiler, same translation unit, different assembly. Calling each with null pointers will behave differently too. Run this with whatever reproducible toolchain you want. Reproducible builds are not about making undefined behavior deterministic, they're a separate and largely unrelated topic.
- 3y ago
- AnimalMuppet 3y agoYeah, no. Yes, in theory undefined behavior can destroy your entire program. In practice? Not so much. I do not care about bogeymen that exist in theory. I don't even care about bogeymen that affect your code. I only care about bogeymen that actually affect my code.
- int_19h 3y agoAs a user, I do care when people who declare that UB is not a problem because "you just have to write good code" still end up repeatedly shipping apps and libraries with vulnerabilities in them. Which with C and C++ specifically happens all the time, and much more often than in languages with significantly less UB. The proof is in the pudding.
- dzaima 3y agoUB is far from the only source of systems not doing the desired thing - writing code that ends up at UB is as wrong as writing code that was written with an incorrect understanding of the invoked behavior. Sure, the neat trick of a+1<a not working is perhaps undesirable, but, even if signed addition was defined to wrap, in most contexts an "a+1" subtracting four billion is not gonna be the specific thing you want it to do in your system. Alternatively, signed overflow could be defined to return exactly 31415, which would be very concrete defined behavior, but barely if at all more useful compared to it being UB.
- AlotOfReading 3y agoI hope I didn't imply that UB was the only source of bugs. It obviously isn't. It's just the only source of bugs that has the side effect of undefining the semantics of all your other code. Just for fun let's take your example and say signed overflow returns integer pi. That now means the compiler has to implement your (hypothetical) next line checking if the result is 31415 rather than omitting it under the assumption that it's unreachable because it would imply UB. All of that code suddenly has defined behavior, even if it's silly.
- dzaima 3y agoBut what does it get you that it's a "defined but completely unusable value" versus "undefined"? Indexing an array by it, adding it to some previously-meaningful value, or doing anything else with it, is still gonna all do practically arbitrary things. I suppose in some cases it can lead to bugs being harder to exploit, but it's still a bug and still wrong and still should be fixed. Being defined is not a get out of exploitability free card. (ok I do have one case where "defined but completely arbitrary" is actually meaningful over "undefined" with no reasonable alternative in C - for a floating-point x, "x==(int)x" for checking if x exactly fits in an int - e.g. gcc on aarch64 or x86+AVX (requiring -fno-trapping-math for whatever reason) optimizes that to "x==floor(x)" as an fp-to-integer cast is undefined on overflowing result)
- AlotOfReading 3y ago
- FpUser 3y ago>"Undefined behavior anywhere means you lack defined behavior everywhere in C/C++." Well, stop programming then. Undefined behavior is everywhere. Your hardware, CPU microcode, any software written in any language etc. etc. >"As an engineer" Your statements suggest otherwise.
- tyfighter 3y agoWhat universe are you working in where you think ANY of that is actually true? In the land of reality where I live and work (I work in hardware), I'm not constructing philosophical prose about well-defined systems. This is another bad faith argument where undefined behavior is made out to be some house of cards. I hate to break it to you, but every computer and all it's software you've ever used is a monument to the glory of undefined behavior, because people just didn't worry about it.
- AlotOfReading 3y agoI'm exceedingly well-aware of how prevalent UB is and how "rarely" it actually turns into an issue in practice. The problem is that you have no way of knowing when or if a particular instance of UB will be dangerous. Even if you somehow know the impact today, that can change without warning in the future. There's a wealth of studies on this subject, like this one [0] documenting cases where undefined behavior leads to miscompilations or examples like [1] where undefined behavior leads to security vulnerabilities. There's a quote from that second link that's deeply applicable here: > This blog post provides an exploit technique demonstrating that treating these bugs as universally innocuous often leads to faulty evaluations of their relevance to security. [0] http://dx.doi.org/10.1145/2517349.2522728 http://dx.doi.org/10.1145/2517349.2522728 [1] https://googleprojectzero.blogspot.com/2023/01/exploiting-null-dereferences-in-linux.html https://googleprojectzero.blogspot.com/2023/01/exploiting-nu...
- tyfighter 3y agoAside from the contrived examples in the paper, the rest are bugs. The kernel exploit was a just a lack of a NULL check; another bug. Bugs are going to happen, and they're going to have unpredictable consequences. What does that have to do with the language and undefined behavior? These are all just more evidence of needing to know what you're doing if you're going to write code at this level, but really not because the vast majority of people aren't writing code where bugs in the form of crashes or security exploits will have serious consequences or can't be fixed.
- 3y ago