CWE-190: Integer Overflow or Wraparound - C
Overview
C performs arithmetic on fixed-width integer types with no automatic overflow detection: signed integer overflow is undefined behavior, and unsigned integer arithmetic silently wraps modulo 2^N. Both are common in allocation-size and bounds-check calculations, where a wrapped result turns into a too-small malloc(), a negative size interpreted as huge by an unsigned parameter, or a bounds check that passes when it should fail.
Common Vulnerable Patterns
Unchecked Multiplication in Allocation Size
#include <stdlib.h>
// VULNERABLE - overflow in allocation size
void *allocate_buffer(int count, int size) {
// Attack: count = 0x40000000, size = 4
// total = 0x100000000 -> wraps to 0 on a 32-bit int
int total = count * size;
return malloc(total); // Allocates 0 bytes; caller then writes count*size bytes into it
}
Why this is vulnerable: The wrap produces a small allocation for a caller that believes it asked for a large one, so the overflow is not the bug - the heap write that follows it is. This is the classic route to a heap buffer overflow, and it is why the allocation and the loop that fills it have to agree on a single computed size.
Signed overflow makes it worse than a wrap. int * int overflowing is undefined behaviour rather than defined truncation, so the compiler is entitled to assume it never happens and may delete a check written afterwards - which is what -Wstrict-overflow exists to warn about. Compute in size_t, test size != 0 && count > SIZE_MAX / size before multiplying, or use calloc(), which performs that check itself.
Overflow in a Bounds Check
#include <string.h>
// VULNERABLE - the overflow check itself overflows
int copy_data(char *dest, int dest_size, char *src, int src_size) {
// Attack: dest_size = 100, src_size = INT_MAX
if (dest_size + src_size < dest_size) { // This addition can itself overflow
return -1;
}
memcpy(dest, src, src_size); // Runs anyway - buffer overflow
return 0;
}
Why this is vulnerable: The guard is written in the form the overflow destroys. dest_size + src_size < dest_size can only be true if the addition wrapped, and because signed overflow is undefined the compiler is permitted to conclude the condition is always false and remove the branch entirely - so the check may not exist in the compiled binary at all.
Rewriting the comparison so nothing overflows is necessary and not sufficient. src_size > dest_size asks the same question without an addition, but with these signed parameters a negative src_size passes it - -1 > 100 is false - and memcpy() takes a size_t, so the conversion turns that into a length near SIZE_MAX. The guard has to reject negatives explicitly before it compares anything:
The better fix is to stop the signed value existing. Declaring both parameters size_t removes the undefined behaviour and the negative case together, because a caller passing -1 produces a value that is genuinely enormous and therefore fails src_size > dest_size rather than sneaking past it. That only holds while the guard compares rather than adds - unsigned wrap is defined but still wraps, so dest_size + src_size is no safer in size_t than it was in int.
Unchecked Addition on User-Controlled Input
#include <stdlib.h>
// VULNERABLE - no upper-bound check before the addition
void process_array(int user_count) {
// Attack: user_count = INT_MAX
int buffer_size = user_count + 1; // Overflows to INT_MIN (undefined behavior for signed int)
char *buffer = malloc(buffer_size); // Negative size converts to a huge size_t
buffer[0] = '\0'; // malloc() returned NULL - null pointer write
}
Why this is vulnerable: INT_MAX + 1 is undefined behaviour, and on a typical implementation it produces INT_MIN. The damage happens at the call: malloc() takes a size_t, so converting a negative int sign-extends it into a value near SIZE_MAX, the allocation fails, and code that does not check the return value writes through a null pointer.
That conversion applies to every size computed as a signed value and passed to an allocator, not only this one. Validate user_count against an explicit maximum before any arithmetic, and keep sizes in size_t from the point they enter the program.
Secure Patterns
Pre-Multiplication Overflow Check
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h> // SIZE_MAX
// SECURE - detect overflow by division before multiplying
bool safe_multiply(size_t a, size_t b, size_t *result) {
if (a != 0 && b > SIZE_MAX / a) {
return false; // Would overflow
}
*result = a * b;
return true;
}
void *allocate_buffer_safe(size_t count, size_t size) {
size_t total;
if (!safe_multiply(count, size, &total)) {
return NULL;
}
if (total > 100 * 1024 * 1024) { // Sanity cap, e.g. 100MB
return NULL;
}
return malloc(total);
}
Why this works: Checking b > SIZE_MAX / a before multiplying detects an overflow that would occur without ever performing the overflowing multiplication itself, so the check can't be bypassed by the same wraparound it's guarding against.
Compiler Builtin Overflow Detection (GCC/Clang)
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h> // SIZE_MAX
// SECURE - hardware-checked overflow detection, no manual math
void *allocate_safe_gcc(size_t count, size_t size) {
size_t total;
if (__builtin_mul_overflow(count, size, &total)) {
return NULL; // Overflow detected
}
if (total > 100 * 1024 * 1024) {
return NULL;
}
return malloc(total);
}
bool safe_add(int a, int b, int *result) {
return !__builtin_add_overflow(a, b, result);
}
Why this works: __builtin_mul_overflow/__builtin_add_overflow (GCC and Clang) use the CPU's own overflow flag to detect wraparound with no extra arithmetic and no risk of the check itself overflowing. C23 standardises the same operations as ckd_add/ckd_sub/ckd_mul in <stdckdint.h> (GCC 14 and Clang 18 onwards), which is the portable spelling to prefer where the toolchain supports it. MSVC has neither; use SafeInt or the intsafe.h helpers there instead.
Enable Compiler and Runtime Overflow Detection
- Build with
-fsanitize=signed-integer-overflow(Clang/GCC UBSan) in development and CI to catch signed overflow, which is undefined behavior and otherwise fails silently. -ftrapv(GCC) traps on signed overflow at runtime; heavier weight than UBSan, typically used for targeted debugging rather than every build.- Neither replaces explicit overflow checks in code that handles untrusted input - they catch bugs during testing, they don't protect a production build that wasn't compiled with them.
Considerations
Where the operands come from decides whether this matters. An overflow needs a value large enough to wrap, so a multiplication of two compile-time constants, or of values already bounded by a prior check, cannot be driven there by an attacker. A length field read from a network packet or a file header can be. Establish which operands are attacker-influenced before adding checks everywhere - the finding is material where at least one is, and noise where none are.
Signed and unsigned overflow are different problems. Unsigned arithmetic wraps, which is defined and testable. Signed overflow is undefined behaviour, so the compiler is entitled to assume it cannot happen and may delete the check you wrote after the fact. That is why the check has to be written to run before the arithmetic rather than by inspecting the result afterwards.
Testing
- Boundary values:
0,1,INT_MAX,INT_MAX - 1,INT_MIN,SIZE_MAX. - Values engineered to overflow:
INT_MAX + 1,SIZE_MAX / 2 * 3, a multiplication whose factors are individually small but whose product exceeds the type's range. - Confirm the safe-arithmetic path actually rejects the overflow case rather than allocating/copying with a wrapped size - test the failure path, not just the success path.
- Run the UBSan-instrumented build against the same test inputs and confirm no signed-overflow reports.
Common Pitfalls
- Checking for overflow after the operation instead of before it:
if (a + b < a)is a valid post-hoc check for unsigned wraparound, but the same pattern on signed integers invokes undefined behavior in the addition itself before the check ever runs - the compiler is free to optimize the check away entirely. Use a pre-check for signed types, and write both halves of it:a > INT_MAX - balone is only correct whenbis positive, becauseINT_MAX - bfor a negativeboverflows in the check itself. CERT INT32-C's form is(b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b). - Using
intfor a size that should besize_t: mixing signedintcounts withsize_t-typed allocation functions creates an implicit conversion where a negativeintbecomes an enormous unsigned value - keep size/count variablessize_t(orsize_t-checked) end to end. - Trusting a sanitizer build to represent production behavior: UBSan/
-ftrapvbuilds catch the bug in testing but are rarely shipped to production for performance reasons - a signed overflow that UBSan flags in CI is still undefined behavior, silently wrong, in an unsanitized release build unless it's also fixed in the source. - Adding an overflow check but skipping the sanity cap: a mathematically correct
safe_multiply()still permits an attacker to request a technically-non-overflowing but enormous allocation (e.g., several gigabytes) - pair the overflow check with an application-appropriate upper bound.