Integer overflow: check the multiply before you allocate

A measuring cup past the rim with coral overflow.

An integer overflow is a calculation that wraps, then a length or index that becomes too small or too large.

CWE-190 is the class. In C it is memory corruption. In an application it can be a price that wraps to a cheap order, or an allocation that becomes a tiny buffer.

The usual mistake is assuming Python or Java integers made this someone else’s problem. Pricing, pagination, and native extensions still wrap or overflow in practice.

This page is where overflow still becomes a security bug, and the checks that belong next to the arithmetic.

MITRE’s CWE-190 page, revision 4.20, still describes the wrap: a calculation produces a value too small, then that value sizes a buffer or a loop. ISO/IEC 9899:2024, C23, added stdckdint.h so ckd_mul can refuse that product. GCC 14 shipped that header in April 2024. Teams still multiply a count by sizeof *p and pass that product to malloc as if the pointer were trusted.

The control is a checked product, a hard cap on n, and a NULL or error path that does not continue into the copy.

When the wrap becomes a write past the heap, continue on the buffer overflow guide. When the bug is a percent-format write, use the format string page. For the rest of the C surface, keep finding and fixing C vulnerabilities next to this one.

CWE-190 is the product that wraps, then sizes the block. The multiply has to refuse that residue before malloc ever sees a number.

SecureCoding

What CWE-190 actually is

CWE-190 is Integer Overflow or Wraparound. The product performs a calculation that can wrap when the logic assumes the result is always larger than the inputs. MITRE’s 4.20 text says the value may become a very small or negative number. The security-critical case is the next line: that number is passed to malloc, new, read, memcpy, or a loop bound.

Unsigned C arithmetic is defined as wrap modulo two to the width. Signed overflow is undefined in C. Neither fact is a defense. A defined wrap that sizes a 0-byte allocation is still a later out-of-bounds write. CWE-680 is the chain name for that second step. This page stops at the multiply so the chain never starts.

Historic reminder, not a recipe: OpenSSH 3.3 computed nresp * sizeof(char*) and passed the product to an allocator. When the product wrapped, the later loop still walked nresp pointers. CERT documented that allocation. The fix class is a checked product and a cap on nresp, not a new way to grow nresp.

CWE-190 is not language-specific. C shows it most often because the allocator takes a raw size_t. Go, JavaScript, and C++ show the same shape whenever you convert a length, shift a count, or multiply before you copy.

malloc(n * size) is the wrap

The dangerous expression is the argument, not the call. n is a count you did not mint. size is sizeof *p or a record length from a header. The star is ordinary C multiplication. If n * size exceeds SIZE_MAX, the argument that reaches malloc is a small residue. The allocator returns a short block. The loop still uses n.

/* BAD: the product can wrap. do not ship this. */
/* item *p = malloc(n * sizeof *p); */

A zero result is the loud case. A small non-zero residue is worse, because the call “succeeds” and the first few iterations look fine. Do not “fix” this by adding one to n or by rounding up to a page. Fix the product.

calloc and a multiply that can say no

POSIX and the C library specify that calloc(n, size) fails when n * size cannot be represented as size_t. That is the paradigm bpineau asked for. It also zero-fills, which closes an uninitialized-read class you did not come here for.

#include <stdlib.h>

/* Named fallback: one helper, one deny path. */
void *alloc_or_none(size_t n, size_t size) {
 if (n == 0 || size == 0) return NULL;
 void *p = calloc(n, size);
 return p; /* NULL means deny. caller must not copy. */
}

Every caller of alloc_or_none checks the pointer. A NULL is a 400 or a clean abort in a privileged daemon, not an unchecked dereference. POSIX requires it. The extra ckd_mul path below does not depend on that libc promise.

OpenBSD’s reallocarray and recallocarray are the same idea for resize. Linux glibc has reallocarray as well. Prefer those over realloc(p, n * size).

brynet’s June 2019 comment on HN 20146385 is the other half of this library story: OpenBSD’s calloc fails when it detects nmemb * size overflow, and reallocarray does the same. Linux overcommit can still return a pointer the kernel cannot back. That is a different failure. Check the product first. Then check the pointer. Then do not write past n.

Go make needs a bound you chose

Go’s make([]T, n) panics if n is negative or too large for the allocator. That panic is not a security boundary you designed. A request header that becomes int(n) can also wrap on conversion before make sees it. The defense is maxItems, then make.

const maxItems = 1 << 20 // 1,048,576. your product number.

func makeItems(n int) ([]item, error) {
 if n < 0 || n > maxItems {
 return nil, errTooMany
 }
 return make([]item, n), nil
}

Do not take n from Content-Length, a protobuf repeated-count, or a JSON array length and pass it through. Decode with a limit. io.LimitReader on the body, Decoder.DisallowUnknownFields where it helps, and a max on the slice you append to. append can grow, but you still stop at maxItems.

cgo and unsafe.Sizeof put you back in C. size := n * unsafe.Sizeof(T{}) wraps like the malloc line. Use a checked multiply, or stay in make with the cap above. binary.Read into a slice you sized from a file header is the same helper: cap, then allocate.

JavaScript bitwise is not math

ToInt32 and ToUint32 are how |, &, <<, and >> work. The product or the shift is reduced modulo 2^32. (n * 4) | 0 is a 32-bit residue, not a byte length. n << 2 is the same class. Typed arrays and Buffer.alloc then see a short length. The loop still uses n.

function bytesFor(n, size) {
 if (!Number.isInteger(n) || n < 0) {
 throw new RangeError("n");
 }
 if (!Number.isInteger(size) || size <= 0) {
 throw new RangeError("size");
 }
 if (n > Math.floor(Number.MAX_SAFE_INTEGER / size)) {
 throw new RangeError("product");
 }
 return n * size;
}

// Named fallback: allocOrNone is the only Buffer constructor this module calls.
function allocOrNone(n, size) {
 const bytes = bytesFor(n, size);
 if (bytes > 32 * 1024 * 1024) return null;
 return Buffer.alloc(bytes);
}

Number.MAX_SAFE_INTEGER is 2^53 – 1. A product past that is not an integer you can trust even before 32-bit bitwise. Buffer.allocUnsafe does not fix the length. It only skips the zero-fill. Use Buffer.alloc after bytesFor.

Do not use n | 0 as a “cast to int” on a length. That is the wrap. Number.isInteger plus the compare above is the check. In TypeScript, n: number does not save you. The runtime check still has to run.

Checked arithmetic you can call

C23 stdckdint.h gives ckd_add, ckd_sub, and ckd_mul. Each stores the mathematical result if it fits in the target type, and returns true on overflow. GCC 14 and Clang 18 expose the header. On a compiler that lacks it, __builtin_mul_overflow is the GNU twin.

#include <stdckdint.h>
#include <stdlib.h>

void *alloc_or_none_ckd(size_t n, size_t size) {
 size_t bytes;
 if (n == 0 || size == 0) return NULL;
 if (ckd_mul(&bytes, n, size)) return NULL;
 return malloc(bytes);
}

alloc_or_none and alloc_or_none_ckd share the same contract: NULL means deny. Pick one helper per tree and grep for raw malloc( of a product. C++ SafeInt or a checked_mul you test is the same idea. Rust checked_mul returns Option. Use the None path. Do not call wrapping_mul on a length.

Languages with big integers still wrap at the FFI. Python int is unbounded. A C extension that does PyLong_AsSize_t and then n * size is back on this page. Validate on the Python side, then check again in C.

SinkWrong productCheck
C malloccount times element widthcalloc or ckd_mul
C reallocn * sizereallocarray
Go makeraw header as ncap, then make
JS lengthn << 2 or | 0bytesFor

Tests that fail closed

You are testing your helper. You are not feeding a public service a hostile count.

// Node: the product must throw. the cap must return null.
const assert = require("node:assert/strict");
assert.throws(() => bytesFor(2 ** 53, 4), RangeError);
assert.equal(allocOrNone(8, 16).length, 128);
assert.equal(allocOrNone(33 * 1024 * 1024, 1), null);
// Go
func TestMakeItems(t *testing.T) {
 if _, err := makeItems(-1); err == nil { t.Fatal("neg") }
 if _, err := makeItems(maxItems + 1); err == nil { t.Fatal("cap") }
 got, err := makeItems(2)
 if err != nil || len(got) != 2 { t.Fatal(err, len(got)) }
}

Grep the tree for the raw product:

rg -n 'malloc\s*\(\s*[^)]+\*' --glob '*.c'
rg -n 'n \* sizeof|<< 2|\\| 0' --glob '*.{c,h,js,ts,go}'

A hit is a review, not an automatic CVE. Replace the product with alloc_or_none, makeItems, or bytesFor. Fuzz your own parser's length field in CI if you already fuzz. Do not fuzz a host you do not own.

Questions we keep getting

Does a bigger integer type fix this?

No. uint64_t n still wraps at 2^64. A 64-bit product of two 32-bit values can be the right check, if you compare before you down-convert to size_t on a 32-bit target. The helper still has to refuse a value that cannot size the copy.

Is saturating math safer than a hard error?

For a length, no. Saturating to SIZE_MAX asks the allocator for more memory than you can use, or for a block you will then under-index. Fail closed. Tell the caller the request is too large.

Do compiler sanitizers replace the helper?

UBSan and -ftrapv catch some signed wraps in debug. They are not on in every production build, and unsigned wrap is defined so sanitizers stay quiet. Ship the check.

Muhammad Luqman / About Author

Information Security Enthusiast | Ethical Hacker | CEH | MS Infosec