4 ms·
"Undefined behavior" in general is a nightmare. After memory safety, the next target should be the enforcement of underflow/overflow trapping. With the exceptio
by lambdaone 3y ago
"Undefined behavior" in general is a nightmare. After memory safety, the next target should be the enforcement of underflow/overflow trapping. With the exception of the intentional use to implement modular arithmetic, underflow/overflow should always be an error condition.
- OrvalWintermute 3y agomost consumer operating systems are inherently non-deterministic which I put right in there next to undefined behavior (some people say it is a different thing).
- conradludgate 3y agoIt is a different thing. Non determinism is different to undefined behaviour. Undefined behaviour can cause miscompiles which can break the expected logic of a program. In the worse case, maybe the compiler optimises away your password validation because it has decided that the failure branch is undefined behaviour. Non determinism can lead to logic bugs, but it can't magically introduce new logic into the program like UB can as part of optimising
- logicchains 3y agoNot everything is a server facing malicious actors. Forcing underflow/overflow trapping would absolutely harm optimisation and performance for use-cases where security is a non-issue.
- brookst 3y agoI want to agree, but I’ve seen too many cases where code written for a narrowly scoped purpose finds its way into a totally different environment where the original assumptions no longer hold. Maybe it’s worth a tax on constrained use cases in the name of having fewer time bombs out there?
- cultureswitch 3y agoThe main problem with signed overflow being UB is that it's an opportunity for compilers to blatantly do adversarial language lawyering against the programmer in order to enable optimisations. When it comes to unsigneds, there's no such problem (and this is in fact the real reason why anything that can be unsigned in C/C++ should be unsigned).
- jdewerd 3y agoYeah. Back when machine architectures were churning so fast that compiler flexibility allowed portability that otherwise wouldn't have happened, UB freedom was a net good. Now that those arbitrary architecture choices have stabilized, the good is no more, and the evil of optimization rules lawyering has replaced it. UB needs to go.
- phkahler 3y ago>> the next target should be the enforcement of underflow/overflow trapping Trapping is nonsense - in production anyway. You might use it in development to catch errors. But in production there is no way to "fix" an overflow if it happens and is detected, so you'd be looking to crash on trap which in some code is preferable to silent data corruption. I do lots of fixed-point math for embedded motor control. Representing rotor angle as a signed 16bit number is deeply engrained in me these days because taking differences to get a signed delta just works, and I never have to worry about rollover because it behaves exactly the way I want. ;-) While this is technically undefined behavior, I've never run across a compiler that worked differently.
- kmeisthax 3y agoRust is already most of the way there. The default math operators still do implicit underflow and overflow, but the behavior is actually well defined: debug builds panic on overflow and release builds wrap on overflow. There is no way to get a C-style "demons fly out my nose" overflow. There's also explicit arithmetic functions that let you choose your overflow behavior: * Checked: return None on overflow * Wrapping: two's compliment wrapped overflow (the "overflowing" variant gives you a carry bit) * Saturating: return maximum value on overflow
- ryao 3y agoDo you mean the C compiler's optimization passes can do strange things to code paths that overflow? The last I checked, C would wrap on overflow too. Rust uses the same compiler backend as C, so it could very well have the same behavior emerge from compiler optimization passes.
- steveklabnik 3y ago> it could very well have the same behavior emerge from compiler optimization passes. It can not, because "compiler optimization passes" is not the root cause of this behavior: it is that it is undefined behavior in the language itself. This is what gives the compiler license to make those transformations. In Rust, it is not undefined behavior, and therefore the compiler does not have the right to make those transformations for Rust code. Just because Rust uses LLVM does not mean that suddenly it inherits C's semantics. This has actually happened (well, more specifically, C++'s semantics, C was accidentally inheriting them as well IIRC) at least one time before, but that is a bug in LLVM that was fixed. Now each language has the proper semantics here, and it all works just fine. (I am referring to the behavior with regards to infinite loops with no side effects.)
- dezgeg 3y agoTry something like this with optimization on: int would_increment_overflow(int a) { if (a + 1 < a) { return -1; return 0; } This gets turned to a function that always returns 0. 0000000000000000 <would_increment_overflow>: 0: f3 0f 1e fa endbr64 4: 31 c0 xor %eax,%eax 6: c3 retq Using unsigned int gives the wrapping semantics.
- alkonaut 3y agoHaving it as default on todays hardware is a non-starter. Few would pay say a 200% perf penalty for arithmetic unless it's on a security critical path. So opt-in it is. And almost all languages already support that type of opt in at the global level or smaller scope.
- jandrewrogers 3y agoYou can have underflow/overflow trapping today if you want it in C++. Most people don't use it, at least not in release builds, because of the performance cost.
- jcranmer 3y agoThe performance cost exists largely because software doesn't try to check for it, therefore no one tries optimizing those checks away.
- trealira 3y agoIt doesn't catch all signed overflow. If you define a function like this: int8_t abs8(int8_t n) { return (n >= 0) ? n : -n; } Even with `-ftrapv`, evaluating abs8(-128) will produce -128, because 127 is the maximum value for a signed 8-bit integer (so trying to get 128 wraps around to -128), but this isn't caught. However, both Rust and Ada do catch this. This article highlights this difference between C and Ada: https://borretti.me/article/signed-integers-asymmetrical https://borretti.me/article/signed-integers-asymmetrical I know Rust catches it because after I read that article, I tested it with Rust in debug mode.
- jandrewrogers 3y agoI wasn’t necessarily assuming ‘-ftrapv’. For reliable software in C++ it is pretty common to have alternative integer type implementations that provide different fully defined behaviors and guarantees than the default integer types. This became legitimately transparent around C++17 IIRC. It is more or less drop-in and mostly produces optimal codegen. Also a good way to eliminate irritating integer behaviors inherited from C. It is easy enough to crank out custom integer types in C++ these days which are arbitrarily safe that there isn’t much excuse for not doing it, particularly since generics and metaprogramming does most of the work. Outside of interfacing with syscalls, there isn’t much use for C primitives beyond size_t.
- trealira 3y ago> For reliable software in C++ it is pretty common to have alternative integer type implementations that provide different fully defined behaviors and guarantees than the default integer types. This became legitimately transparent around C++17 IIRC. It is more or less drop-in and mostly produces optimal codegen. Also a good way to eliminate irritating integer behaviors inherited from C. I didn't know this. I can imagine you can create your own integer types as wrappers over existing primitives, and define them so that, e.g., on signed overflow, the program aborts or wraps (and your signed integers are actually a wrapper class over unsigned integers, but with operator overloading so that they act like signed integers). Is that what you mean? If not, could you show me what you mean?