5 ms·
It may be purposefully constructed for the example, but it is also easily constructed. This is only possible because the compiler deletes code. Yes, you can ge
by gavinhoward 3y ago
It may be purposefully constructed for the example, but it is also easily constructed. This is only possible because the compiler deletes code.
Yes, you can get messed up control flow from modular arithmetic code, but the compiler is not going to delete that code if it happens to overflow.
And just because it's purposefully constructed doesn't mean it won't happen in real code, which is much more complicated and liable to triggering such problems.
- UncleMeat 3y agoThe compiler deletes code all the time in well formed programs. You typically want this. I do agree that the "but I put the check there" argument is real and that it absolutely breaks the principle of least surprise that you could look at your source and see a check that is important yet doesn't actually end up in the binary. But basically nobody has provided a rigorous conception of what a compiler shouldn't do w.r.t. undefined behavior. "Just don't delete my code" doesn't work since that's a big portion of what makes compiler optimization valuable. "Don't ever optimize based on UB assumptions" doesn't work, since that means that literally any function without a body available to the current translation unit can havoc all memory (or worse, even another function running concurrently).
- gavinhoward 3y ago> "Don't ever optimize based on UB assumptions" doesn't work Why not? Why is that somehow worse than code that breaks everywhere, all the time? Correctness should always come first. Sometimes performance is part of correctness (real-time), but it is not most of the time. When performance is not a part of correctness, why would optimizing for performance even be a possibility? And having done real-time work, I can tell you that the output of the compiler is scrutinized heavily in that domain. If the compiler is optimizing according to UB, changes would be made. But to answer your question, this is what I would do: If a programmer did not opt-in to the UB, don't optimize according to the assumption of lack of that particular UB. Programmers do not opt-in to signed overflow as UB, so it should never be used as an optimizing assumption. Programmers do opt-in to UB for data races by sharing memory across threads or signal handlers, so compilers can optimize against that. Programmers do not opt-in to non-terminating loops as UB, so compilers should not optimize to that. Programmers do opt-in to strict aliasing by using pointers to different types, or by using the `restrict` keyword, so the compiler can optimize on that. You may complain that if I use signed types, I'm opting-in to that UB. I disagree, but even if you are right, I'm explicitly opting-out by using unsigned types. And yet, you'll still complain about that; you started this discussion by complaining about developers using unsigned types because it would be slower, even though you acknowledged that the bugs wouldn't be as bad. In other words, you want performance at any cost, so much so that you'll complain when people use mitigations against compiler optimizations that can raze code like Rome razed Carthage. So you and I will never see eye-to-eye because you firmly believe that performance is everything, and I firmly believe that correctness is everything. I usually try to be diplomatic on HN (and often fail), but this is one time I can confidently say: you are dead wrong. By the way, I have public code that uses unsigned types as much as possible, and it still holds its own against an implementation that uses a heavily optimized library. So you are also wrong that unsigned types make it noticeably slower.
- UncleMeat 3y ago> Why not? Why is that somehow worse than code that breaks everywhere, all the time? Imagine this code int x = 0; f(); return x; In a world where you can't optimize based on UB assumptions, x can change in the body of f. So instead of returning the constant zero, you need to allocate space on the stack, store to it, and then read from that location after f returns. Arbitrary pointer arithmetic means that any function can havoc all memory. Even worse, this can happen in another thread. You don't need to share memory across threads to opt in to data races. You can access whatever you want with pointer arithmetic if you want to be correct in the presence of UB. Or this code void do(foo* f) { if (f == nullptr) return; f->do(); } f could be deleted. This could be UB. You need runtime tracking of deleted memory locations (not even objects, since we can set up arbitrary aliases in our UB-defensive world). You can use ASan for this, but even that isn't guaranteed to detect the UaF so you are going to need even more overhead. Even worse, you can't do this if you've got multiple threads. You don't even have to have shared f across threads, you just need to have shared something else and let the other thread do some pointer arithmetic. Any runtime check that f is not deleted can be raced. > So you and I will never see eye-to-eye because you firmly believe that performance is everything I do not believe this. I am one of the people at my company fighting to spend performance to buy safety. I just don't believe that the principle of least surprise can be implemented in the standard in the way some people say and I don't believe that UB is totally unique from other forms of incorrectness in a way that should cause us to behave very differently about it. > I firmly believe that correctness is everything. Do you run your production binaries with all of the sanitizers enabled?
- gavinhoward 3y ago> Imagine this code...In a world where you can't optimize based on UB assumptions, x can change in the body of f. Yeah, so you allocate space on the stack (which is free, by the way, because space has to be allocated anyway for the new function's frame), store to it, and load from it. An extra store and an extra load. Of stack space that was probably already in cache anyway. So what? What a miniscule price to pay. [0] > You don't need to share memory across threads to opt in to data races. You can access whatever you want with pointer arithmetic if you want to be correct in the presence of UB. So C should have bounds checks and define a failing bounds check as an abort. And then such optimizations could be correct. That's what so many other languages do, and it works. > Or this code...f could be deleted. This could be UB. Do you mean freed? Yes, that could be UB. But the compiler should never say, "Oh, well, f could be freed here, so I'm just going to delete the `f->do()` call." Bad example because compilers don't currently take advantage of it. They day they take advantage of that is the day that I quit programming. > I do not believe this. I am one of the people at my company fighting to spend performance to buy safety. Ha! I do not believe you at all! You are okay with compilers taking advantage of UB to elide a store and a load. The only way you are telling the truth is if your company is full of malicious compiler authors. > I just don't believe that the principle of least surprise can be implemented in the standard in the way some people say It totally could. C could even have bounds checks without changing the ABI. They just don't. > I don't believe that UB is totally unique from other forms of incorrectness in a way that should cause us to behave very differently about it. It's the only form of incorrectness that: * Will destroy everything. * That compiler authors claim they have a right to take advantage of. Yeah, we definitely should treat it differently. You are dead wrong. > Do you run your production binaries with all of the sanitizers enabled? No, because I don't work in the industry. But in my personal code, I wrote my own bounds checks and enabled them. In C. In release mode. All your talk of "but you could make a pointer to anywhere" doesn't happen in my code. I use structured concurrency to allocate and free items in only one place, in the same stack frame. When threads are created, they prevent the creating thread from returning from the stack frames that the children may have gotten pointers to. I fuzz like you wouldn't believe and make sure all paths come back clean in ASan, UBSan, TSan, and Valgrind, including memory leaks. I add all unique paths from fuzzing to my test suite. I'm going to write my own malloc() that will have double-free and use-after-free checks. I use unsigned types to avoid signed overflow, and such unsigned types more easily trip the bounds checks when they overflow because the bounds checks only need one condition. Tell you what: I would like you to take version 6.7.2 of my bc [1] and get it to execute UB. Any UB is fine. If you do, I'll enable sanitizers on my release builds of all of my software. But you won't, and that's why I can forego sanitizers on release builds: I do enough work to ensure that UB won't happen. And that's why I use unsigned types. Your bellyaching about their poor optimization will not change that. [0]: https://gavinhoward.com/2023/10/he-who-gives-up-correctness-for-performance-deserves-neither/ https://gavinhoward.com/2023/10/he-who-gives-up-correctness-... [1]: https://git.gavinhoward.com/gavin/bc https://git.gavinhoward.com/gavin/bc