go: proposal: spec: change all int types to panic on wraparound, overflow
I know this has been discussed before, but I didn’t see a specific proposal filed for it yet and I think it’s important.
Unexpected integer overflow can lead to serious bugs, including bugs in Go itself. Go’s bounds-checking on slices and arrays mitigates some of the harmful effects of overflow, but not all of them. For example, programs that make system calls may pass data structures into the kernel, bypassing Go’s usual bounds checks. Programs that marshal data-structures to be sent over the wire (such as protocol buffers) may send silently-corrupted data instead of returning errors as they ought to. And programs that use unsafe to access addresses with offsets are vulnerable to exactly the same overflow bugs as in C.
In my experience, Go programs and libraries are often written assuming “reasonable inputs” and no overflow. For such programs, it would be clearer for overflow to cause a run-time panic (similar to dividing by zero) rather than silently wrapping around. Even in the case where the unintended overflow is subsequently caught by a slice bounds check, reporting the error at the overflowing operation rather than the slice access would make the source of the bug easier to diagnose.
The potential performance impact of this proposal is similar to bounds-checking in general, and likely lower than using arbitrary-precision ints (https://github.com/golang/go/issues/19623). The checks can be omitted when the compiler can prove the result is within bounds, any new branches will be trivially predictable (they’ll occupy some CPU resources in the branch-predictor but otherwise add little overhead), and in some cases the checks might be able to use bounds-check instructions or other hardware traps.
For the subset of programs and libraries that intentionally make use of wraparound, we could provide one of several alternatives:
- “comma, ok” forms <strike>or “comma, carry” forms (https://github.com/golang/go/issues/6815)</strike> that ignore overflow panics, analogous to how the “comma, ok” form of a type-assertion ignores the panic from a mismatched type.
- Separate “integer mod 2ⁿ” types (requiring explicit conversions from ordinary integer types), perhaps named along the lines of
int32wraporint32mod. - Implicit wrapping only for unsigned types (
uint32and friends), since they’re used for bit-manipulation code more often than the signed equivalents.
Those alternatives could also be used to optimize out the overflow checks in inner-loop code when the programmer has already validated the inputs by some other means.
[Edit: added this section in response to comments.]
Concretely, the proposed changes to the spec are:
Integer operators
For two integer values x and y, the integer quotient q = x / y and remainder r = x % y satisfy the following relationships:
[…]
<strike>As an exception to this rule, if the dividend x is the most negative value for the int type of x, the quotient q = x / -1 is equal to x (and r = 0).</strike>
[…]
The shift operators shift the left operand by the shift count specified by the right operand. They implement arithmetic shifts if the left operand is a signed integer and logical shifts if it is an unsigned integer. The result of a logical shift is truncated to the bit width of the type: a logical shift never results in overflow. Shifts behave as if the left operand is shifted n times by 1 for a shift count of n. As a result, x << 1 is the same as x*2 and x >> 1 is the same as x/2 but truncated towards negative infinity.
[…]
Integer overflow
If the result of any arithmetic operator or conversion to an integer type cannot be represented in the type, a run-time panic occurs.
An expression consisting of arithmetic operators and / or conversions between integer types used in an assignment or initialization of the special form
v, ok = expr
v, ok := expr
var v, ok = expr
var v, ok T1 = expr
yields an additional untyped boolean value. The value of ok is true if the results of all arithmetic operators and conversions could be represented in their respective types. Otherwise it is false and the value of v is computed as follows. No run-time panic occurs in this case.
For unsigned integer values, the operations +, -, *, and << are computed modulo 2ⁿ upon overflow, where n is the bit width of the unsigned integer’s type. Loosely speaking, these unsigned integer operations discard high bits <strike>upon overflow</strike>, and programs may rely on ``wrap around’'.
For signed integers, the operations +, -, *, and << are computed using two’s complement arithmetic and truncated to the bit width of the signed integer’s type upon overflow. <strike>No exception is raised as a result of overflow. A compiler may not optimize code under the assumption that overflow does not occur. For instance, it may not assume that x < x + 1 is always true.</strike>
If the dividend x of a quotient or remainder operation is the most negative value for the int type of x, evaluation of x / -1 overflows and its result upon overflow is equal to x. In contrast, evaluation of x % -1 does not overflow and yields a result of 0.
[…]
Conversions between numeric types
For the conversion of non-constant numeric values, the following rules apply:
- When converting between integer types, if the value is a signed integer, it is sign extended to implicit infinite precision; otherwise it is zero extended. If the value cannot be represented in the destination type, an overflow occurs; see the section on integer overflow. Upon overflow, the result is truncated to fit in the result type’s size. For example, if
v := uint16(0x10F0), thenw, _ := uint32(int8(v))results inw == 0xFFFFFFF0.
This proposal is obviously not compatible with Go 1, but I think we should seriously consider it for Go 2.
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 61
- Comments: 74 (70 by maintainers)
Commits related to this issue
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013) and op... — committed to wking/hive by wking 5 years ago
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013) and op... — committed to wking/hive by wking 5 years ago
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013), opens... — committed to wking/hive by wking 5 years ago
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013), opens... — committed to wking/hive by wking 5 years ago
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013), opens... — committed to wking/hive by wking 5 years ago
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013), opens... — committed to wking/hive by wking 5 years ago
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013), opens... — committed to csrwng/hive by wking 5 years ago
- *: Bump to install-config v0.12.0 Catching up with openshift/installer@dafc79f0 (Generate Network.cluster config instead of NetworkConfig.networkoperator, 2019-01-15, openshift/installer#1013), opens... — committed to wking/hive by wking 5 years ago
From a programmer’s point of view, proposal #19623 is a significant simplification (wrap around and overflow disappear, and the new
inttype is both simpler in semantics and more powerful in use), while this proposal is a significant complication (wrap around and overflow now panic, and the newinttype is both more complex in semantics and more difficult to use).One of Go’s ideas is to reduce the intrinsic complexity of the programming language at hand while at the same time provide mechanisms that are general and powerful and thus enable programmer productivity and increased readability. We need to look at language changes not from a compiler writer’s point of view, but from a programmer’s productivity point of view. I think this proposal would be a step backward in the philosophy of Go.
I am also not convinced about the claim that overflow checking is “likely much lower than using arbitrary-precision ints”: The cost of arbitrary precision ints is there when one actually uses them, otherwise their cost is similar to what needs to be done for bounds/overflow checking (it’s the same test, essentially). There’s a grey area for ints that use all 64 (or 32) bits as they will become “big ints” internally (at least one bit is usually reserved to implement “tagged ints” efficiently) - but in code that straddles this boundary and if it matters one might be better off using a sized
intxxtype anyway. Finally, there’s a GC cost since the garbage collector will need to do extra work. But all that said, dealing with overflow panic will also require extra code, and that has to be written by each programmer by hand. It is also much harder to verify/read that code.I’m not in favor of this proposal.
@griesemer
I believe that the two proposals are compatible (and even complementary). We could make
intanduintarbitrary-precision types (to make default behavior simpler), and also make the sized integer types panic on overflow (to make complex behavior safer).Could you elaborate on this point? I would expect that most code would either not handle the panic, or use a wrapping integer type explicitly. Even the latter option does not seem like a lot of extra code.
An arbitrary sampling:
Swift traps overflows by default and provides a set of parallel overflow operators (for example,
&+means “+ignoring overflow”).Rust traps overflows in debug mode, and provides explicit wrapping, saturating, checked, and overflowing variants. The overflowing variant is delightfully similar to what I’m proposing here. The Rust folks are keeping a handy list of overflow bugs they’ve found since adding the checks in debug mode.
C# has a
checkedkeyword that enables overflow checks; overflows are silent by default.Clojure throws an
ArithmeticExceptionby default; it can be disabled by setting*unchecked-math*or using explicit functions likeunchecked-add.The Kotlin reference doesn’t talk about overflow at all. In this thread the suggestion to check overflow appears to be rejected on efficiency grounds.
D overflows silently, but provides a
checkedintlibrary that sets a boolean on overflow.And of course Java “[does] not indicate overflow or underflow in any way”, but Java 8 added
Math.addExactand related functions, and the SEI CERT coding standard requires detection or prevention of overflow except for bitwise operations and “carefully documented” benign usage (such as hashing).I think either proposal is an improvement over the status quo. Programmers who assert the nonexistence of overflow will write the same code they do today, so no cognitive overhead there, and I won’t have to worry about silent errors if they’re wrong.
@nnemkin Thanks for the reply. You did answer my question but I phrased my question incorrectly. Your suggestion would give me the wrong answer, the answer that this issue is designed to avoid. What I’m looking for is the expression that says “if I do this operation, will it panic?” That is, I want to not only avoid getting the wrong answer, I also want to avoid panicking. In general code should not panic. If we change the language as proposed, then people writing carefully will need to avoid the panic on overflow, and perhaps return an error instead. They need a reasonably simple way to do that that does not involve a deferred function calling recover.
I note that for every case in Go that currently causes a run time panic there is a straightforward way to avoid the panic: write
p != nilori < len(s)ordivisor != 0and so forth. What is the straightforward way to avoid arithmetic overflow in the typeint, for the six operations that can overflow (I’m not counting++or--)?Separate question: should we treat
x >> yas an overflow if the value ofyis greater than the number of bits in the type ofx?@griesemer
I want to come back to this point: I agree with it and I agree that it’s important. In fact, it is essentially the whole motivation behind this proposal.
Implicit wraparound favors the compiler-writer over the programmer. Let me explain.
The things that implicit wraparound makes easier are:
Implementation of hash functions.
Hash functions ignore overflow because they’re preserving the overflowed information in the other bits of the computed hash.
We’ve got lots of hash functions in the standard library (in
hash/andcrypto/), but how many have you seen in user code? Even when programmers implement their own hash-based data structures (currently very difficult due to https://github.com/golang/go/issues/15292), they should generally be using an existing “good” hash function rather than trying to “roll their own”. Fast, low-collision hash functions are notoriously difficult to get right.So “writing hash functions” is usually the complier-writer’s task, not the programmer’s task.
Implementation of multi-word integer types
Multi-word integer implementations (such as
big.Int) ignore overflow because they’re preserving the overflowed bits in the next word.I believe that you yourself are the author of much of
math/big. So implicit overflow makes your job a bit easier — but how many application programmers do you think are writing that sort of code, especially given the existence ofmath/big?Compiler code generation
Most CPUs running Go code today implement two’s-complement integer arithmetic and don’t trap on overflow. So if you define the language to implement two’s-complement arithmetic that ignores overflow, it’s easy to generate efficient code in the compiler: you don’t have to generate or optimize the code to check for overflows, and you don’t have to worry about writing optimizations to reduce the need for overflow checks.
The things that implicit wraparound makes harder are:
Programs that count things reliably
If you’re writing programs that deal with 32-bit timestamps, you want to avoid Y2038 bugs, such as silently adding a future calendar entry at a point in the distant past. If you’re writing programs that display view counts, you want to avoid treating popular items as negatively popular. If you’re writing programs that tally votes or compute financial statements, you’d really rather detect when you’ve hit the limits of your program rather than, say, accidentally treat a $3B expenditure as a credit. If you’re serving user data, you’d really rather detect invalid document IDs than accidentally serve unrelated documents with the same ID mod 2^32.
Programs that marshal and unmarshal large data structures
There have been multiple
protobufbugs due to overflows in the encode/decode paths. For example, see https://github.com/google/protobuf/issues/1731 and https://github.com/google/protobuf/issues/157 in the public repo — the latter being a potential DoS attack. Even in the cases in which Go’s slice bounds checks protect against that sort of bug, it’s not good for Go marshalers to generate payloads that might potentially corrupt or crash downstream clients or servers.I don’t know about you, but to me, “counting things” and “marshaling data” seem much more representative of “programmer” use-cases. So I think you’ve got it backwards: the status quo favors compiler writers, and detecting overflow by default would push the balance more toward general programmers.
Is it possible to add this type into go1.9? Just add a type called uint64OverflowPanic may be enough to start try this stuff. I think We may need both overflow panic uint64 and non overflow panic uint64 in go 2.
Let me put it this way: I don’t think we’ve seen a good answer to my question about avoiding panics other than subsuming #6815.
Wrap-around arithmetic is necessary for crypto and other algorithms, but panic on overflow is very useful for security and correctness (i.e. not continuing silently with invalid data).
The balance can be achieved if only signed arithmetic panics on overflow (option 3 in the proposal). App logic dealing with quantities should use the default type
intor appropriately large type for the task (int64,int128). Binary math algorithms can happily continue to useuint,uint64,uintptretc.The cost of signed overflow checking is minor. It is also a lot easier to optimize statically and locally, compared to arbitrary precision math proposal. There’s no fallback path and overflow branches can be inserted late, all targeting the same
paniccall (one per function, to distinguish them). SingleJOafter everyADD+ a dozen dead bytes per function should give a pretty realistic performance impact estimate.FWIW both GCC and Clang have
-ftrapv, and Clang also has-fsanitize=signed-integer-overflow.This doesn’t work for division by zero, but for addition, subtraction, and multiplication
s, ok := a op bcan assign useful values to s in even when ok is false. For subtraction and addition, the result is what you’d get right now. For multiplication, I think also “what you get now”, which is (I think) the low-order bits of the overflowing multiplication.For the add/sub case you can directly compute what the answer should have been; for multiplication, you have the low-x-low part of your ultimate result.
We could do this, without the panics, in Go 1, which both gives people a clear way to write this now, and future-proofs it against panic-on-overflow in Go 2 (because you wrote this where you thought it would happen). It would come into minor conflict with a Go 2 “int just grows as needed” int type (but not the sized types) – either it always returns “okay” or we statically error out on that case, and we could conceivably remove them automatically (if the “ok” were involved in a complex expression we’d have to be a little more careful).
Maybe just 0 – the zero value for the type as with type assertions? https://play.golang.org/p/CBYt8ISMAu
(I’m not thinking of implementation details though)
@bronze1man A type called
uint64OverflowPanicwould be counterproductive. Users who are spelling out verbose type-names are presumably already thinking about overflow, and at that level of verbosity they can just as easily make calls to some library to check the operations.The point of this proposal is to make detection of overflow the default behavior. That’s why it’s a language proposal and not just a library.
@bcmills Perhaps. My point is that this all adds extra complexity to the language where I am not convinced that we need more. We need less. Most people couldn’t care less about overflow and simply want integers that “just work”.
I think you have misunderstood the proposal. You would only need one assignment per “expression consisting of arithmetic operators and / or conversions between integer types”, not one per operation: the check applies to the whole expression tree.
In the current draft, the only place you have to chop up the expression tree is at function-call boundaries, and even that could perhaps be relaxed.
The proposed
, okextension here would make the fix for #21481 much clearer. Instead of:we could write
An interesting idea.
One implementation approach would be to steal a “tag bit” to indicate overflow, but that has the downside of requiring all the fixed-width integer types to be n-1 bits (
int31instead ofint32). My experience with languages in the ML family is that that’s really annoying: pretty much the whole point of fixed-width integer types is to interoperate with other languages and to make wire-encodings easy to work with, and those overwhelmingly tend to use power-of-two integer sizes.Another implementation approach would be to use the arbitrary-length-integer trick of using the tag bit to indicate a pointer (or an offset in a table somewhere) that points to the real value, which would still be power-of-2-sized. My intuition is that that’s likely to be strictly more overhead than trapping on overflow, but perhaps not fatal. But it would imply that tainted Go ints could not be used directly in
cgoor kernel structs, which in turn implies that “Go integer” and “C integer” probably need to actually be separate types in the type system.So, maybe not terrible in terms of performance, but a bit awkward.
My bigger concern is that it would make overflow checking substantially weaker: we would only detect overflows that result in a validity check somewhere, which means we could still end up serializing corrupted values (e.g. incorrect length stamps in framing messages) and triggering arbitrary bugs in the (non-Go) programs on the receiving end.
@nathany
The Ariane 5 explosion is a nice example, but note that it involved many layers of failures:
The Ariane 501 Failure Report has a lot of interesting analysis, but I want to call out the second recommendation in particular:
With silent overflow, it is easy to miss overflow errors even with fairly comprehensive tests: comparisons of overflowed values may work out anyway during tests because they happen to wrap around to values that satisfy some of the same invariants. (For example, the overflow in https://github.com/golang/go/issues/20687 was not caught in previous testing because the inputs happen to overflow to nonnegative values.)
On the other hand, a panic is hard to miss. It can be accidentally or intentionally recovered (e.g. by the
fmtorhttppackage), but in most cases a test that triggers a panic will fail, visibly and with an easy-to-debug stack trace. With visible test failures, overflow bugs are more likely to be caught before reaching production.@ianlancetaylor
Note that people writing carefully already need to avoid data corruption on overflow today: the need for these checks in carefully-written code is more-or-less orthogonal to the detection of that overflow.
Another sometimes-not-straightforward case is avoiding panics from closing already-closed channels. But that is another matter. The general point is well-taken.
FWIW, only checks 3 and 4 strike me as reasonably readable, and both require fairly substantial checks to the language.
For increasing precision without math.Big and without arbitrary-precision ints, here’s a (mostly) tongue-in-cheek suggestion: add built-in [u]intNN types for arbitrary power-of-two NN. No more arguing about where to stop! 😃
@bronze1man Do you have reason to think that the cost of JSON encoding is dominated by integer arithmetic? On a modern CPU many algorithms are dominated by the time it takes to load values from memory, and that should not change under this proposal.
Just a datapoint, the code in https://golang.org/src/runtime/hash64.go takes advantage of overflow in almost every line of source (for the uintptr type).
Maybe we can’t do this until Go 2, but we can do experiments to get some data about it now. We could hack panic on overflow into the compiler today. What happens performance-wise with the go1 benchmarks? How many false positives are there? How many true positives are there?