Towards Optimization-Safe Systems: Analyzing the Impact of Undefined Behavior
pdos.csail.mit.edu
pdos.csail.mit.edu
In the Java memory model you still get memory-safety in the presence of race conditions, but other than that the program behavior is pretty much undefined. It's better than undefined behavior in C, but not by much.
I assume everyone reading a discussion about undefined behavior knows what nasal demons are, but just in case someone doesn't: in illustrating just what "undefined behavior" means in C, it has been suggested that it would be perfectly legal for a compiler, upon encountering such, to cause demons to fly out of the programmer's nose. The point being that it's not just a matter of producing an unexpected value or something of that nature, but a completely unexpected behavior. As a real-world example, early versions of gcc would, upon encountering a #pragma (whose behavior is undefined) would start a game of nethack.
I know that sounds a bit like a cop out, but it lays the finger on the pain point of modern computing, that whatever we do, we are still so heavily reliant on C for everything low level.
How would we come close? I'd say name a language whose JIT directly outputs (x86) assembly, and whose runtime is written in a runtime free version that is output by that same JIT.
The only language I know of that can do that atm is Rust.
And with intelligent coding you could probably get those checks to be very minimal.
But sooner or later you'll come across a programmer who isn't aware of wrapping and wrongly assumes the value can only grow when a positive integer is added to it. So you could define a language which silently does bignums and all is well, except now you have to deal with memory allocation (this can be a major issue on memory-constrained platforms) during integer arithmetic, and the way bignum computations affect the program's performance & timing (crypto side channel attacks anyone?). So the next programmer again has to know the pitfalls of the Defined behavior and Do The Right Thing, just as he must do now with Undefined Behavior (i.e. rearrange bounds checking and pointer arithmetic so that it'll never point beyond one past the last element of an array; and so that your signed integers never overflow).
The second snippet (pointer dereference before NULL check) is perhaps more subtle, and again I might agree that the language would be safer if it didn't allow the removal of the NULL check that would keep this incorrect code from doing further harm. However, no matter what your proposed defined behavior for NULL/invalid pointer dereferencing is (what would it be? I have no idea), there would likely be cases where the programmer makes the wrong assumption and his program relying on that Defined behavior is still incorrect and potentially insecure.
Simply defining undefined behavior just doesn't fix incorrect code. You still have to remove the broken (undefined or not) code from the codebase...
EDIT: If you meant to suggest languages whose compiler will abort if it finds code that might result in undefined behavior, well, that's much harder. UB in C is predominantly a run-time issue, so you'd be making a really smart static analyzer to detect at compile time whether your code can actually hit UB. Did someone solve the halting problem? Alternatively you can make a language where integers never overflow. Then we get to what I said about bignums and surprise-allocation and such.. Haskell just isn't for everything.
For the sort of code that was best written in C, a few decades ago, it was often reasonable engineering to depend on technically undefined behavior. Then over the decades the compilers made increasingly-subtle transformations without getting much better at telling you where they might have tripped you up (again, maybe except for clang which I haven't tried yet).
For a taste of how common undefined behaviour is, even in important code bases, (and why everybody should be using tools to test for it) see this post: http://blog.regehr.org/archives/1054 "Integer Undefined Behaviors in Open Source Crypto Libraries"
Also the follow up about how difficult C sometimes makes it to avoid undefined behaviour without a performance penalty: http://blog.regehr.org/archives/1063 "Safe, Efficient, and Portable Rotate in C/C++"
Perhaps this will help find these coding mistakes? (unfortunately, it seems to be a runtime checker and not a compile-time warning)
It still intrigues me that this happens, I do wonder if it is limited to languages like C or if it could conceivably happen in anything with an advanced runtime (think the porter stemmer bug that broke java7 on release)
Maybe we can introduce this to jit-spraying ? (http://dsecrg.com/files/pub/pdf/HITB%20-%20JIT-Spray%20Attac...)
It so happens that this particular research project found a few industrial users eager to try it out and study the feasibility of deploying it, but it wasn't necessary for the research project to exist.
Take an oversized shift, for example. The result is undefined, and for good reason. This allows an expression like x << y to be translated directly to a single machine instruction on just about every architecture. If there was a defined result for oversized shifts, then many architectures' instructions would not match it, forcing the compiler to generate inefficient code on those architectures.
For every example of a weird optimization causing problems, you can find an example of that same optimization making code go much faster.
You could say that compilers should value correctness over speed, and I would agree with you, but: all of these optimizations are correct. These are C compilers, and they compile C programs just fine. Where they have trouble is on programs that aren't actually valid C, but are just close. You can argue that they are being technically correct in a bad way, but then if you abandon the standard and decide that you should "correctly" compile a certain class of invalid programs, how do you decide what "correct" means?
0xffffffff << 0 is 0xffffffff
0xffffffff << 1 is 0xfffffffe
..
0xffffffff << 30 is 0xc0000000
0xffffffff << 31 is 0x80000000
It is defined to ignore overflow. Therefore, I claim that there's enough consistency here that we can extend the pattern to this: 0xffffffff << 32 is 0x00000000
I claim this really is the mathematically correct answer. It's consistent with the most obvious pattern, and there are no other relevant patterns.C has chosen to call this undefined behavior instead of returning the correct answer here, and I call that choosing optimization over correctness.
With C, you could still use the shift instruction on that architecture. With your 'correct answer', the compiler would have to output instructions to check the value of x prior to the shift.
Besides which, shifting is just one specific case. C has an easily understandable and documented behavior: overflow is undefined, no matter how you achieve it. We don't have to go into horrific detail about what happens when you overflow with adds, multiplys, shifts and so on.
Bitshifting is an operation that people generally expect to be extremely fast. Adding a check on every single operation generated by a C compiler just to guarantee identical results upon overflow probably would not go over well.
If you shifted one way a few bits then shifted back a few bits (As you would in calculating addresses for graphics operations), the optimizer would think you were undoing the operation and do nothing. or interestingly shift n-m bits.
You had to do some dummy operation in between for the code to actually execute. This was true at least up to Watcom 11.
"Fixing" C would be creating a new dialect, unfortunately. (Personally, I just consider C a last resort, and I've found that works pretty well.)