Proven C Book한국어 GitHub

81 How to ask about overflow — <stdckdint.h>

What to know first

chapter 27, Integers · the finiteness of integers
chapter 52, Undefined behaviour · overflow is outside the contract

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

Chapter 27 taught that integers are finite; chapter 52 taught that overflow is outside the contract. This chapter is C23′s answer to the problem those two make. Putting the answer after the problem is obvious; putting it fifty chapters after is a choice — so that the danger of hand-rolled checks is met first.

By the end of this chapter

We learn the checked arithmetic C23 brought in. Why hand-written code that “checks after calculating whether it overflowed” is dangerous, exactly what 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

  1. 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.

  1. 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 <- 300 confirms it.
  2. 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”.
  3. Even on overflow the result is stored. That value is the value wrapped round into the result type. The example’s INT_MAX + 1 remaining as -2147483648 is that — and it matters that even for a signed value it is defined behaviour in this place.
  4. Unsigned wrap-round is reported as “overflowed” too. The example’s 3u - 5u confirms 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.

placethe old idiomnow
array allocationthe check n && SIZE_MAX/n < szckd_mul(&bytes, n, sz)
growing a bufferjust calculating cap * 2ckd_mul(&cap2, cap, 2)
joining lengthslen1 + len2 + 1ckd_add twice
index calculationbase + off just sockd_add (compulsory for signed values)
numbers from inputusing it straight after atoistrtol (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))
#endif

Only 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 rememberthe point
checking afterwardswith signed values the check itself is outside the contract — it can be erased
the shapebool overflowed = ckd_add(&result, a, b)
the criteriondoes the mathematical result fit in the result type
the result typesettled by the first argument. not swayed by promotion rules
even on overflowthe wrapped value is stored (defined behaviour)
unsigned valueswrap-round too is reported as “overflow”
one expressiondo not mix the judgement and the result
first place to apply itallocation size calculation
where it is absentmake 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.