81 How to ask about overflow — <stdckdint.h>
What to know first
Looking back
Chapter 7 said unsigned overflow is defined wrap-round while signed overflow is outside the contract, and chapter 66 said calloc checks the product of the element count and the size. Then how do we write the check for whether a multiplication overflows ourselves?
A. That very “checking ourselves” is the code that has been written wrongly for half a century. With signed values, checking after it has overflowed is itself already outside the contract, so the compiler may erase the checking code (chapter 13′s editor), and with unsigned values the idiom that inserts a division is hard to read and easy to get wrong. C23 tidied this place up at the level of the language.
The need for this chapter, and its context
By the end of this chapter
ckd_add, ckd_sub and ckd_mul promise, and how to move the size calculations of existing code onto this tool.The questions this chapter answers
- Are the
ckd_*functions or macros? Do they not evaluate arguments several times?
81.1 The trap of checking afterwards
We start from the most common hand-written check.
Counter-example. Adding and then seeing whether it overflowed
int sum = a + b;
if (sum < a) { /* it overflowed */ } /* ← this check can vanish */If a and b are signed integers, the moment a + b overflows it is already undefined behaviour. The check after it has meaning only on the premise “if it did not overflow”, so the compiler judges that sum < a can never be true and erases the conditional entirely. This pattern really did make checks vanish quietly in several projects, and among them were security checks.
For unsigned values, wrap-round being defined, the check above does work. But going to multiplication makes even that awkward.
if (n != 0 && bytes / n != sz) { /* it overflowed */ } /* correct but hard to read */So compilers each put out extensions — GCC’s and Clang’s __builtin_add_overflow family, MSVC’s SafeInt, the home-made macros of many projects. They worked well but were not portable, and to secure portability every project had to write the same shell again. It is the same pattern as chapter 65′s strlcpy story — reality finds the answer first and the standard ratifies it belatedly.
81.2 C23′s answer — ckd_add, ckd_sub, ckd_mul
examples-en/ch81/ckdint.c
/* Overflow received not as a value but as "did it happen" — C23 <stdckdint.h> */
#include <limits.h>
#include <stdckdint.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
/* The common allocation sum: n elements x size sz. If the multiplication overflows, the vessel comes out too small. */
static void *alloc_array(size_t n, size_t sz)
{
size_t bytes;
if (ckd_mul(&bytes, n, sz)) { /* true means it overflowed */
printf(" the size computation overflowed - nothing is allocated\n");
return NULL;
}
return malloc(bytes);
}
int main(void)
{
/* the test and the reading of the result are written apart (mixed in one expression there is no order) */
int r;
bool over = ckd_add(&r, INT_MAX, 1);
printf("INT_MAX = %d\n", INT_MAX);
printf("ckd_add(INT_MAX, 1) overflow? %s r = %d (the wrapped value)\n", over ? "yes" : "no", r);
over = ckd_add(&r, 1, 2);
printf("ckd_add(1, 2) overflow? %s r = %d\n", over ? "yes" : "no", r);
/* even with mixed types the judgement is made on the mathematical value */
signed char c;
over = ckd_add(&c, 200, 100);
printf("signed char <- 300 overflow? %s c = %d\n", over ? "yes" : "no", c);
/* unsigned subtraction: wrapping is defined behaviour, yet this still reports "it overflowed" */
unsigned u;
over = ckd_sub(&u, 3u, 5u);
printf("unsigned <- 3 - 5 overflow? %s u = %u\n", over ? "yes" : "no", u);
printf("\nallocation sums\n");
void *ok = alloc_array(1000, sizeof(int));
printf(" 1000 x %zu -> %s\n", sizeof(int), ok ? "allocated" : "refused");
free(ok);
void *bad = alloc_array(SIZE_MAX / 2, sizeof(int));
printf(" SIZE_MAX/2 x %zu -> %s\n", sizeof(int), bad ? "allocated" : "refused");
free(bad);
return 0;
}
Output
INT_MAX = 2147483647
ckd_add(INT_MAX, 1) overflow? yes r = -2147483648 (the wrapped value)
ckd_add(1, 2) overflow? no r = 3
signed char <- 300 overflow? yes c = 44
unsigned <- 3 - 5 overflow? yes u = 4294967294
allocation sums
1000 x 4 -> allocated
the size computation overflowed - nothing is allocated
SIZE_MAX/2 x 4 -> refused
The way to read it is this. All three macros have the same shape.
bool overflowed = ckd_add(&result, left, right);There are four promises.
- It calculates in infinite precision and then puts it in the vessel. The judgement is “does the mathematical result fit in the result type”, and there is no overflow in the intermediate calculation. So it judges exactly even when the arguments’ types differ from each other, and even when the result type is narrower than the arguments — the example’s
signed char <- 300confirms it. - The result type is settled by the first argument (the pointer). It means the arguments’ promotion rules (chapter 28) do not sway the result, so there is no need to fret over “in which type is it calculated”.
- Even on overflow the result is stored. That value is the value wrapped round into the result type. The example’s
INT_MAX + 1remaining as-2147483648is that — and it matters that even for a signed value it is defined behaviour in this place. - Unsigned wrap-round is reported as “overflowed” too. The example’s
3u - 5uconfirms it. The value itself is a defined result (4294967294), but checked arithmetic reports that it differs from the mathematical value. In size calculations this is the property needed.
Q. Are the ckd_* functions or macros? Do they not evaluate arguments several times?
A. What the standard settles are macros, but they are pinned down not to evaluate their arguments several times (implementations mostly expand them into compiler builtins). So code such as ckd_add(&r, i++, j) is safe too.
But there is another trap. The judgement and the result must not be mixed inside one expression.
printf("%s %d", ckd_add(&r, a, b) ? "overflow" : "fine", r); /* dangerous */There is no settled order between the moment r is read and the moment ckd_add writes to r, so the old value may be printed (chapter 20′s story of ordering). This mistake really did print a wrong value when this chapter’s example was first written. Take the judgement into a variable, and use the result on the next line — that one line of discipline is the whole of it.
81.3 Where it is used — size calculation comes first
The alloc_array in the latter part of the example is the type. Calculating an allocation size is the place where checked arithmetic is most sorely needed. If n * sz overflows you end up obtaining a small vessel and using it believing it big, which leads straight to a buffer overflow accident. Several famous vulnerabilities took exactly this route.
| place | the old idiom | now |
|---|---|---|
| array allocation | the check n && SIZE_MAX/n < sz | ckd_mul(&bytes, n, sz) |
| growing a buffer | just calculating cap * 2 | ckd_mul(&cap2, cap, 2) |
| joining lengths | len1 + len2 + 1 | ckd_add twice |
| index calculation | base + off just so | ckd_add (compulsory for signed values) |
| numbers from input | using it straight after atoi | strtol (chapter 66) + a range check |
Table 82.1
The last line matters. Checked arithmetic only catches the overflow of a calculation; it does not filter out a value that was too large to begin with. The check at the input-parsing stage (chapter 66) and the check at the calculation stage do not stand in for each other.
A common misconception. “Use ckd_* and worry about integer overflow ends”
Three things remain. First, division is not in this header — INT_MIN / -1 is still an outside-the-contract case you must block yourself. Second, conversions are not checked. Assigning from a wide type to a narrow one is not arithmetic but conversion (chapter 7′s truncation), so a value being wrecked there is not ckd_*‘s business. Third, floating point is not its subject (chapter 73).
In summary, checked arithmetic is a tool answering the narrow and clear question “did an addition, subtraction or multiplication overflow its vessel”. It is a good tool precisely because it is narrow.
81.4 Where this tool is absent
In an environment that cannot yet use the C23 header, prepare in two steps.
#if defined(__has_include)
# if __has_include(<stdckdint.h>)
# include <stdckdint.h>
# define HAVE_CKDINT 1
# endif
#endif
#ifndef HAVE_CKDINT /* fill in with the GCC/Clang extensions */
# define ckd_add(r, a, b) __builtin_add_overflow((a), (b), (r))
# define ckd_sub(r, a, b) __builtin_sub_overflow((a), (b), (r))
# define ckd_mul(r, a, b) __builtin_mul_overflow((a), (b), (r))
#endifOnly beware that the argument order differs — the standard puts the result pointer first, the compiler builtins put it last. Gathering such shells in one place is the real shape of the portability layer spoken of in chapter 57, and Part XII’s library does the same work.
Platform note. The road of leaving the checking to tools
There is also a way of catching overflow without mending the code. GCC’s and Clang’s-fsanitize=signed-integer-overflow (UBSan) catches overflow during execution and reports it, and -ftrapv stops the program on overflow. Both are for testing — they show themselves only in a run in which an overflow actually happened, so they are different in character from checked arithmetic, which “blocks in code the places where it could happen”. It is the same conclusion as chapter 17′s story of debuggers: tools help observation but do not stand in for the contract.Recap
| to remember | the point |
|---|---|
| checking afterwards | with signed values the check itself is outside the contract — it can be erased |
| the shape | bool overflowed = ckd_add(&result, a, b) |
| the criterion | does the mathematical result fit in the result type |
| the result type | settled by the first argument. not swayed by promotion rules |
| even on overflow | the wrapped value is stored (defined behaviour) |
| unsigned values | wrap-round too is reported as “overflow” |
| one expression | do not mix the judgement and the result |
| first place to apply it | allocation size calculation |
| where it is absent | make a shell with __builtin_*_overflow |
Table 82.2
We have learned how to ask about overflow according to the contract. The next chapter is this part’s last and the most conspicuous change C23 made to the language — the story of things that were macros becoming keywords.