Skip to content

Integer Overflows and Format-String Bugs, Explained

How integer overflow, truncation and signedness errors turn into memory corruption, why user-controlled format strings are dangerous, and the checks and warnings that prevent both.

Published on 5 min read

Integer bugs and format-string bugs are rarely dangerous on their own. They become dangerous when their result feeds a memory operation: a length passed to memcpy, a size passed to malloc, an index into an array, or a string passed to printf as the format. This guide covers both classes from the defender's side: the vulnerable patterns, the compiler features that catch them, and the coding rules that make them impossible. It complements stack buffer overflows and heap corruption, which are frequently the second half of an integer bug.

Integer bugs

C integers have fixed widths, mixed signedness and implicit conversions. Four failure modes account for most real-world bugs:

Failure modeExampleResult
Overflow / wraparoundcount * sizeof(struct item) exceeds SIZE_MAXA tiny allocation for a huge count
Truncationuint16_t len = strlen(s);Length silently reduced modulo 65536
Signedness confusionint len from input is negative, passed to memcpy as size_tA negative value becomes an enormous size
Underflowsize_t remaining = total - used; with used > totalWraps to a value near SIZE_MAX

The allocation-size overflow

This is the canonical integer-to-heap bug (CWE-190 leading to CWE-122):

/* Vulnerable: the multiplication can wrap on 32-bit size_t or with huge counts. */
struct item *load_items(uint32_t count, const uint8_t *src) {
    struct item *items = malloc(count * sizeof(struct item));
    if (!items) return NULL;
    for (uint32_t i = 0; i < count; i++)
        parse_item(&items[i], src, i);   /* writes far past a small buffer */
    return items;
}

If count * sizeof(struct item) wraps, malloc succeeds with a small buffer and the loop writes count full items into it. The fix is to check the arithmetic before trusting it:

#include <stdckdint.h>   /* C23; GCC 14+, Clang 18+ */

struct item *load_items(uint32_t count, const uint8_t *src) {
    size_t bytes;
    if (count > MAX_ITEMS) return NULL;                      /* domain limit first */
    if (ckd_mul(&bytes, (size_t)count, sizeof(struct item))) /* true on overflow */
        return NULL;
    struct item *items = malloc(bytes);
    ...
}

On older toolchains, __builtin_mul_overflow(a, b, &res) in GCC and Clang does the same job. Simplest of all, calloc(count, sizeof(struct item)) performs the overflow check internally and zeroes the memory.

Signedness and the negative length

/* Vulnerable: len is signed; a negative value passes the check. */
int copy_field(char *dst, size_t dst_size, const char *src, int len) {
    if (len > (int)dst_size) return -1;  /* -1 is not > dst_size */
    memcpy(dst, src, len);               /* -1 converts to SIZE_MAX */
    return 0;
}

Fix it by making lengths unsigned from the moment they enter the program, and validating both ends:

int copy_field(char *dst, size_t dst_size, const char *src, size_t len) {
    if (len > dst_size) return -1;
    memcpy(dst, src, len);
    return 0;
}

GCC and Clang warn about many of these conversions with -Wconversion and -Wsign-conversion. They are noisy on legacy code, so a common strategy is to enable them for new modules and for the parsing layer first.

Undefined behaviour makes checks disappear

Signed overflow is undefined behaviour, so the compiler is allowed to assume it never happens. A check written in terms of the overflow itself can be optimised away:

/* Broken check: the compiler may delete it, since a + b "cannot" overflow. */
if (a + b < a) return -1;

/* Correct: check before doing the arithmetic. */
if (b > 0 && a > INT_MAX - b) return -1;

The rule is simple: never detect overflow by performing it. Use the checked-arithmetic builtins or compare against limits beforehand.

Tooling for integer bugs

Tool or flagCatches
-fsanitize=signed-integer-overflow (UBSan)Signed overflow at runtime
-fsanitize=unsigned-integer-overflow (Clang only)Unsigned wraparound (not UB, but often a bug)
-fsanitize=implicit-conversion (Clang)Truncating and sign-changing implicit conversions
-ftrapvTraps on signed overflow (older, less precise than UBSan)
-Wconversion -Wsign-conversionRisky conversions at compile time
CodeQL / CoverityTainted values reaching allocation sizes and indices

The sanitizers guide shows how to combine UBSan with ASan in a test build.

Format-string bugs

printf-family functions interpret their first argument as a small program: conversion specifiers such as %s, %x and %p tell the function how many further arguments to read from registers and the stack, and how to interpret them. If an attacker controls that first argument, they control how the function walks the argument area.

The vulnerable pattern

/* Vulnerable: user input is used as the format string. */
void log_user(const char *msg) {
    printf(msg);
}

/* Fixed: the format is a constant; user input is data. */
void log_user(const char *msg) {
    printf("%s", msg);
}

With the vulnerable version, input containing %x or %p makes printf print values it was never given, which leaks stack contents and, often, addresses that defeat ASLR. The %n specifier is worse: instead of printing, it writes the number of characters output so far to a pointer taken from the argument list. That is why this bug class was historically rated as severe as a buffer overflow. See the glossary entry on format-string vulnerabilities for a short definition.

The same bug appears with fprintf, sprintf, snprintf, syslog, err/warn, and, most often in modern code, custom logging wrappers that forward a caller-supplied string as the format.

Defences that make it hard to write

  • Warnings. -Wformat -Wformat-security flags calls where the format is not a string literal and there are no other arguments; -Wformat=2 adds -Wformat-nonliteral and more. Debian, Ubuntu and Fedora enable -Wformat -Wformat-security in their default package build flags, and several turn it into an error with -Werror=format-security.
  • Annotate your wrappers. Tell the compiler which argument is the format so the same checks apply to your own logging functions:
void log_msg(int level, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));
  • FORTIFY_SOURCE. With -D_FORTIFY_SOURCE=2 or higher, glibc's checked printf variants abort if %n appears in a format string located in writable memory, which neutralises the classic write primitive.
  • Positional and width checks. Fortified builds also verify that positional arguments (%1$s) are used consistently.
  • Code review rule. A format argument must be a string literal or come from a trusted, constant table. There are no exceptions worth the risk.

How these bugs show up in triage

Integer bugs usually present as something else: an ASan heap-buffer-overflow whose allocation size looks absurdly small, or a memcpy with a size near 0xffffffffffffffff. When you see that, walk back to the arithmetic that produced the size. UBSan builds pinpoint the exact expression:

parser.c:41:25: runtime error: unsigned integer overflow:
  4294967295 * 24 cannot be represented in type 'unsigned int'

Format-string bugs present as garbled or hexadecimal-looking log output, crashes inside vfprintf, or *** %n in writable segment detected *** from a fortified binary. Each of those is a code fix, not a configuration issue. The crash report guide covers the triage workflow.

Checklist

  • Represent sizes and lengths as size_t from the point they enter the program, and validate them against documented maxima.
  • Never detect overflow by performing it. Use ckd_add/ckd_mul, __builtin_*_overflow or pre-checks against limits.
  • Use calloc or checked multiplication for every count * size allocation.
  • Build with -Wall -Wextra -Wformat=2 -Werror=format-security, and -Wconversion on parsing code.
  • Annotate logging wrappers with __attribute__((format(printf, ...))).
  • Run tests with -fsanitize=address,undefined and fuzz the parsers that compute sizes.

Related guides

0x1000 · Vulnerability Classes

Stack Buffer Overflows Explained for Defenders

Why writing past a stack buffer is dangerous, the C patterns that cause it, how compilers and sanitizers catch it, and the fixes and mitigations that contain it.