5 ms·
Author here. > It barely scratches the surface. I agree. The point of the post is not to enumerate and explain the implications of all 283 uses of the word "u
by thomashabets2 5mo ago
Author here.
> It barely scratches the surface.
I agree. The point of the post is not to enumerate and explain the implications of all 283 uses of the word "undefined" in the standard. Nor enumerate all the things that are undefined by omission.
The point of the post is to say it's not possible to avoid them. Or at least, no human since the invention of C in 1972 has.
And if it's not succeeded for 54 years, "try harder", or "just never make a mistake", is at least not the solution.
The (one!) exploitable flaw found by Mythos in OpenBSD was an impressive endorsement of the OpenBSD developers, and yet as the post says, I pointed it at the simplest of their code and found a heap of UB.
Now, is it exploitable that `find` also reads the uninitialized auto variable `status` (UB) from a `waitpid(&status)` before checking if `waitpid()` returned error? (not reported) I can't imagine an architecture or compiler where it would be, no.
FTA:
> The following is not an attempt at enumerating all the UB in the world. It’s merely making the case that UB is everywhere, and if nobody can do it right, how is it even fair to blame the programmer? My point is that ALL nontrivial C and C++ code has UB.
- muvlon 5mo agoFair enough! > And if it's not succeeded for 54 years, "try harder", or "just never make a mistake", is at least not the solution. And I 100% agree. UB is way overused by these standards for how dangerous it is, and as a consequence using C (and C++) for anything nontrivial amounts to navigating a minefield.
- saagarjha 5mo agoWhat should the behavior above be defined to do?
- lll-o-lll 5mo agoHCF
- JadeNB 5mo agoWhat is that?
- FabHK 5mo agoA fictitious assembly instruction (and pretty good TV series). https://en.wikipedia.org/wiki/Halt_and_Catch_Fire_(computing) https://en.wikipedia.org/wiki/Halt_and_Catch_Fire_(computing...
- SAI_Peregrinus 5mo agoHalt and Catch Fire
- saagarjha 5mo agoI have good news about what UB allows
- jeffffff 5mo agoCompilation error
- echoangle 5mo agoCouldn’t you just define that function arguments are evaluated left to right? Or just throw an error.
- saagarjha 5mo agoI meant reading the uninitialized variable
- muvlon 5mo agoThere is no uninitialized variable, I explicitly initialized it to 5. And yes indeed, C could do what Rust does and define the order of evaluation for function arguments. If the argument expressions are indeed side-effect-less, the compiler can always make use of the "as-if" rule and legally reorder the computation anyway, for example to alleviate register pressure.
- jfoks 5mo agoWhy? Just for this edge case? It could be faster and/or allow smaller code size to allow this to be undefined. Undefined is also different from "depends on the compiler", because which behavior is chosen can even depend on the circumstances, whatever code appears before and/or after it. That said, UB in code, such as this example of ordering of reads of volatile parameters being undefined, does not automatically mean that code that uses it is bad. It may very well be that the function being called doesn't misbehave either way.
- echoangle 5mo agoThat’s the point of the whole article. It’s not worth the speed gain to have a language that nobody can safely use because you can’t really prevent UB when you write it. > It may very well be that the function being called doesn't misbehave either way. The function being good or bad has nothing to do with the UB. The UB occurs before the function is called.
- Filligree 5mo agoPrint x twice. Not all “side effects” care about order. Better yet, define an order for parameter evaluation.
- poppadom1982 5mo agoYou're missing the point. Volatile forces two loads of a value that may have changed in the middle. So the value of "x" may depend on the time/order of load.
- hmry 5mo agoWhy is that missing the point? Loading it twice, possibly with different values, is the intended behavior. It's only undefined because the C spec doesn't specify the order of the loads (unlike most other languages which have a perfectly well-defined order for side effects in a single expression).
- rowanG077 5mo agoWhat you are describing is implementation defined behavior. Using that is perfectly safe and reasonable. Undefined means this programs is malformed.
- hmry 5mo agoNo I'm just repeating what the original comment said, which is that it's explicitly UB: "5.1.2.4.1 says any volatile access - including just reading it - is a side effect. 6.5.1.2 says that unsequenced side effects on the same scalar object (in this case, x) are UB. 6.5.3.3.8 tells us that the evaluations of function arguments are indeterminately sequenced w.r.t. each other." If function arguments were sequenced with respect to each other, it wouldn't be a problem. But actually, maybe the original comment is wrong. Presumably "indeterminately sequenced" and "unsequenced" mean different things, although I don't have a copy of the standard at hand to check.
- AnimalMuppet 5mo ago
- tsukikage 5mo ago“Implementation defined behaviour”: compiler author chooses, and documents the choice. A lot of UB should be implementation defined behaviour instead; this would much better match programmers’ intuitions as they reason about their code - you can even see it in the comments of this post: it’s always things like “this hardware supports / doesn’t support unaligned accesses”, it’s never nasal demons.
- tardedmeme 5mo agoI told someone at a conference that UB actually means "implementation-defined, no documentation required". He started to refute me and then stopped.
- marcosdumay 5mo agoExcept that UB doesn't mean that. UB means "the developer must never write this".
- munch117 5mo agoBoth are wrong. It means "this standard does not constrain the behaviour of code that does this". It's entirely legal for implementations to have predictable behaviour, documented or not, for code that is undefined by the standard. In their quest for maxxing benchmark performance they generally choose not to, but there's really nothing in any standard that stops you from making an implementation that prioritises safety.
- tardedmeme 5mo agoEvery implementation so far has predictable behavior in all cases. Sometimes the rules for predicting it are very obscure. But it's all fully defined within the compiler's binary code. And none of them link to nasal portals.
- AlotOfReading 5mo ago
- stellamariesays 5mo ago[flagged]
- webstrand 5mo agoI think as compilers got smarter, UB changed somewhat in meaning. Originally the compilers didn't perform such complex analysis, and while invoking UB could break your program, it would still do something reasonable.
- marcosdumay 5mo agoYes, but compilers got smart enough for it to be a problem around 30 years ago, and we are still arguing about what to do.
- pjmlp 5mo agoYou see a reasoning here, basically when all those C compiler benchmarks started, vendors moved from what Frank Allen described, to anything goes to win SPEC something benchmarks. "Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue.... Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels? Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities." -- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming
- lelanthran 5mo ago> The point of the post is to say it's not possible to avoid them. Or at least, no human since the invention of C in 1972 has. What are you talking about? UB was coined only in the first C standard, in 1989. Prior to that there was no "If you do this, anything can happen". It was "If you do this, that will happen".
- professoretc 5mo agoMore like, "if you do this, what happens depends on your particular combination of hardware, operating system, and compiler. Don't ask us."
- nickez 5mo agoNo, that would be implementation defined.
- tekne 5mo agoNo, that's actually UB. The important bit here is "compiler defined" -- UB means the compiler is allowed to assume it never happens while compiling. Consider, for example, an implementation defined function f() -- which can also diverge/crash horribly, etc. If I write if p { print("p is true") } else { g() } if p { f() } Then either we: - print p is true and execute f - do nothing This is true regardless of if f immediately crashes the computer, nasal demons, whatever -- that's implementation defined. UB means f may never happen. And that means the compiler may optimize this to just: g() Notice the difference here -- the print never happens!, and g always happens. You can see why this is concerning when you write code like if dry_run { print("would run rm -rf /") } else { run("rm -rf /") } if dry_run { // oops: some_debug_string is NULL and will segfault! print(some_debug_string); }
- tyg13 5mo agoI see what you're going for, but I don't see how your example is UB. If `p` is a pointer, and, after your `if (p)` check, `p` is dereferenced unconditionally, then yes, your check for `p == NULL` could be removed, and the code under the `if` would be removed as well. But the example you've constructed is not UB.
- wahern 5mo ago> Now, is it exploitable that `find` also reads the uninitialized auto variable `status` (UB) from a `waitpid(&status)` before checking if `waitpid()` returned error? (not reported) I can't imagine an architecture or compiler where it would be, no. I presume you're referring to this code: pid = waitpid(pid, &status, 0); if (WIFEXITED(status)) rval = WEXITSTATUS(status); else rval = -1; The only signal handler find installs is for SIGINFO, and it uses the SA_RESTART flag, so EINTR can be ruled out. The pid argument is definitely valid as you can't reach the above if it wasn't, and there's no other way for the child process to be reaped[1], so no ECHILD. A check should probably be added in case the situation changes in the future, triggering spooky action at a distance, or were that code to be copy+pasted somewhere where the invariants didn't hold. But I think the current code in its current context is, strictly speaking, correct as-is. [1] OpenBSD lacks the kernel features for such surprises that might theoretically be possible on Linux.
- thomashabets2 5mo agoIndeed. That's why I didn't deem it worth reporting. But in my code, I would have fixed for the reasons you mention. Sprinkle enough of these around, and some low percentage will in the future have its assumption invalidated.
- BobbyTables2 5mo agoCouldn’t waitpid return EINTR if the (parent) process were stopped and then continued? EINTR scares the crap out of me because nobody expects it!
- wahern 5mo agoNo. You only get EINTR when a signal handler fires and you didn't use the SA_RESTART flag with sigaction. If you don't install any signal handlers, or you use SA_RESTART on all handlers, or you've blocked/masked all signals (or at least the ones with handlers), you won't get EINTR. When writing library code, it's important to consider EINTR because you can't know about signal dispositions. Though, the common practice of looping on EINTR kind of defeats the purpose.
- saghm 5mo ago> if nobody can do it right, how is it even fair to blame the programmer? My point is that ALL It's fair to blame the programmer for the choice of programming in a language like this, if it was in fact their choice. As you've so eloquently put, choosing those languages is essentially equivalent to choosing UB, so starting a new project with one of them is 100% blameworthy when the UB is inevitably found.
- thomashabets2 5mo agoNot all projects are green field. But sure, new modules can be written in other languages. And C is, as cross-language barriers go, fairly easy to interface with.
- nextaccountic 5mo ago> Or at least, no human since the invention of C in 1972 has. No human without proper tools maybe, but what about seL4? It goes beyond proving the code is UB-free and actually formally verifies the code works as intended. And the code is written in C. (the proofs of course aren't) The proof is interesting because it goes beyond just proving the C code is correct. For some platforms, they compile the code with an ordinary compiler, and verify that the machine code does what the C code is supposed to do. (that's because just writing correct C code doesn't help you if you trigger a compiler bug) This works even if the compiler (in this case, GCC) isn't verified - they verify a specific output of the compiler, not that the compiler always generates machine code correctly.