back

by uecker·2y ago·view on hn ↗
Regarding signed overflow, I find it extremely useful that it is UB which allows me to instruct the compiler to insert a trap. This is very helpful to find bugs. For the same reason, the Linux kernel community (or some of them) want an attribute what would make overflow of annotated unsigned types also be UB.
1 comments
I can do that without UB. In fact, I implemented wrapping, trapping, and saturation semantics in my arithmetic.
It can be done as a library, but that is far less ergonomic than having the checks for actual arithmetic operators.

However, I think that consistency should win here. Given that unsigned arithmetic already wraps around - and this cannot be changed for backwards compatibility reasons - making signed arithmetic do something different is just bad design. It would certainly be better if the default behavior was to trap rather than wraparound or saturate, but, well, we're talking here about a language that is >50 years old, so that ship has sailed.

It would sure be nice to get proper dedicated operators for trapping and saturating arithmetic in some future version of the C standard, though.

> It can be done as a library, but that is far less ergonomic than having the checks for actual arithmetic operators.

You are correct, but I care about correctness more. I will use a lobrary to get it.

That said, having such checks by default would surely reduce the possible impact of overflow on security, so I would support it.

What is consistent depend on the perspective. For me signed integers are a model for integers and overflow means that I exceeded the capabilities of the machine to model those correctly. Unsigned is a model for modulo arithmetic. Wrapping signed integer would not model anything useful.
If signed ints were truly a model for true mathematical integers, they would be unbounded (as in e.g. Python).

C is low-level enough that it's not particularly useful to think in those terms, IMO. If you're writing in C, it's usually for one of the two reasons: either you want to be "close to the metal" because you're doing low-level stuff, or it's a legacy codebase. From the first perspective, I can't think of any "modern" - as in, past three decades - architecture for which signed wraparound is not the default & fastest behavior. From the second perpective, a lot of legacy code actually assumes signed wraparound (because compilers used to just defer to what the hardware actually did).

FWIW the reason why C had UB for signed overflow historically is because back when it was being standardized, there were still machines around that used something other than two's complement to represent signed values. The original ANSI C89 straight up refused to specify the signed representation, and C99 narrowed it down to several options; thus, the obvious hardware implementation would in fact produce different results on overflow. Conversely, unsigned integers always had a well-defined representation for which wraparound was simply the natural hardware behavior. I don't think this was ever meant to have some kind of higher meaning.

Given that C23 explicitly mandates two's complement for signed now, IMO, the signed overflow behavior should just be made consistent with that as well as real-world hardware. The only claimed downside to this is that compilers can no longer "optimize" code that was previously broken but would now be well-defined, but I don't see why that is an actual problem.

You realize that I am part of the standard's committee? ;-)

I am programming C because Python is too slow for my use case (and too annoying and to unstable) and yes, I use C's integer types as an abstraction for mathematical integers. And I would say this is the case in almost all C code I see and most programmers I talk to. How would two-complement's wrapping for int be useful for anything? It does not model anything useful IMHO.

And the problem with compilers not optimizing is that people want them to optimize... Compilers not being able to to do this as well anymore would indeed be an actual problem for many people.

It would be extremely useful e.g. because you could perform overflow checks as easily as:

  if (x + 1 < x) // overflow
as opposed to the much more complicated dance that you have to do today to be conformant.

I don't disagree that people want compilers to optimize in general. But can you give an example of optimization that is 1) relies on compiler treating signed overflow as UB, and 2) is actually useful (i.e. makes conforming code run faster)?

As far as modeling, the very fact that C ints are bounded - and lest we forget, unless you use `long long` everywhere, even `long` doesn't guarantee you more than 32 bits per the Standard, which is not all that large! - they are not a good abstraction for mathematical integers. C developers use it as such in practice because it is convenient, but consequently tend to ignore the overflow behavior, not the least because dealing with signed overflow is so inconvenient. Even today, I suspect that the vast majority of production C code out there is not actually safe wrt to signed overflow-induced UB due to overly large inputs (e.g. how many CLI tools do such validation for values that come out of argv?).

if( x > INT_MAX - n ) // overflow for x + n

Your definition of a far more complicated dance is absurd.

Signed integer being undefined makes a lot of integer math faster actually: https://kristerw.blogspot.com/2016/02/how-undefined-signed-o...

There's a reason zig made unsigned overflow undefined: it allows for more optimisations (Zig can also afford it thanks to their wrapping addition (+%) operator).

> I use C's integer types as an abstraction for mathematical integers.

Nothing on you, but in my mind, this is a sign of just how brain dead and unprofessional our industry is.

Using hardware integers as mathematical integers is like using aluminum instead of titanium for a high-stress part in a fighter aircraft. Sure, it does just fine in normal flight; what's the big deal?

And then the pilot needs to pull a 10-g maneuver, putting 10 times the stress on that part. And then the pilot dies because the aircraft breaks apart in midair.

If you need mathematical integers, use GMP. Or implement your own. It isn't hard. I implemented rationals using 32 bytes and no allocations when integers are no bigger than hardware integers.

Adding all of that with this:

> You realize that I am part of the standard's committee? ;-)

makes me geuinely fear for the direction of C.

I think I may need to write boringcc to rescue C from itself. https://groups.google.com/forum/m/#!msg/boring-crypto/48qa1k...

Look at any C program. What are integers used for? Counting things, loop indexing, etc. These are all semantics of mathematical integers. Yes, it is an approximation that only works as integers do not get too big. But the limits are still bigger than most numbers people do use in their daily lives. Most people could not calculate with higher number in their head, are you saying their mental model of integers is a small integer model different from mathematical model? This misunderstands the purpose of mathematical abstractions at a fundamental level.

Big number integers also has use cases, but this is has very special purposes. And also big numbers are not "true" mathematical numbers if you want to be really pedantic, because if you memory is limited also this abstraction breaks downs at some point. Models for true mathematical integers do not exist in the physical world, so that "int" isn't one, is meaningless pedantry.

tinycc is such a boringcc compiler
This is great. But why do you care about what that standard says?

Making it UB in ISO C ensures that no portable program can rely on a specific behavior and this is what makes it possible to find bugs this way, because it is plausible to assume that a program with overflow is buggy. This is also why this does not work for unsigned. We can easily change a compiler to trap on unsigned wraparound. But this is mostly useless because there would be far too many false positives.

> Making it UB in ISO C ensures that no portable program can rely on a specific behavior

This is true.

> and this is what makes it possible to find bugs this way

No, this is not true. In fact, it is the opposite of true.

If a program cannot rely on behavior, it cannot rely on having the behavior that causes the bug to be visible.

It does not need to be able to rely on having the behavior that causes the bug to be visible. We just need tools that make the bug visible, e.g. the undefined behavior sanitizers. Those tools work extremely well for signed overflow, but not for unsigned, exactly because unsigned wraparound is defined and for this reason many programs use it. So you can not distinguish between intended wraparound and incorrect wraparound.

For this reason, signed overflow is essentially a solved problem, while unsigned wraparound will be the source for many interesting bugs - exactly because it is not UB!

> We just need tools that make the bug visible, e.g. the undefined behavior sanitizers.

Does the compiler faithfully compile the source code with sanitizers? Of course not, so we still cannot rely on them.

> So you can not distinguish between intended wraparound and incorrect wraparound.

This is exactly why I implemented both types in my arithmetic. The one I used is the one I intended.

A compiler which compiles with a sanitizer can be perfectly conforming, i.e. faithfully compile a program.
That is not what I mean. I mean that the compiler follows the programmer's intent.