Skip to content

CWE-190: Integer Overflow or Wraparound - Python

Overview

Python's built-in int is arbitrary precision - it never wraps or overflows the way a C int or a Java int/long does, so the classic "wrapped-to-a-small-or-negative-value" failure mode does not occur in pure Python arithmetic. That does not make unbounded integers safe by default: an attacker-controlled value used to size a bytearray, a list, or a loop count can still cause a resource-exhaustion denial of service, and any value that eventually crosses an FFI boundary into a fixed-width C type (NumPy, a C extension, ctypes) reintroduces the classic overflow risk on the C side.

Common Vulnerable Patterns

An unbounded product used as an allocation size

def allocate_buffer(count, size):
    # VULNERABLE - no upper bound; "won't overflow" is true but not the same as "safe"
    # Attacker: count = 10**6, size = 10**5
    total = count * size            # Python computes this exactly, however large
    return bytearray(total)         # Attempts to allocate 10**11 bytes (100 GB) -> MemoryError / DoS

Why this is vulnerable: the arithmetic is correct - Python's int is arbitrary precision, so total really is 1011 and no wraparound occurs. The weakness is that the exact answer is then used to size an allocation with no ceiling of its own. That makes this the resource-exhaustion shape rather than the wraparound shape, closer to CWE-400 (uncontrolled resource consumption) than to the rest of this page; it is here because it is what the classic overflow becomes in a language whose integers do not wrap, and because it is the pattern a reader porting the C advice will get wrong.

The contrast is not that the fixed-width version is safer. In C or Java the product wraps to a small number, the allocation succeeds at that small size, and the writes that follow run past the end of it - memory corruption, which is the more severe of the two outcomes and the one this CWE is named for. Python trades that failure for attempting the allocation it was genuinely asked for.

What that costs depends on where it lands, which is why "it raises MemoryError, so it is handled" is not a fix. An allocation large enough to fail immediately is the good case, and the largest requests are the safest ones here: a total above sys.maxsize never reaches the allocator at all, because bytearray() rejects it with OverflowError: cannot fit 'int' into an index-sized integer before asking for a byte. The damaging range is the one below that ceiling, where the request is a legal size the machine cannot meet. An allocation it can almost satisfy drives the process into swap and takes the host down with it, and on Linux the OOM killer may choose a different process entirely. MemoryError is also raised at an arbitrary point in the middle of the work, so any partially built state is left behind. The fix is an explicit ceiling on count and size before the multiply, chosen from what the application actually needs.

Fixed-width arithmetic at a C boundary

import numpy as np

def total_bytes(width, height, bytes_per_pixel):
    # VULNERABLE - NumPy's fixed-width dtypes overflow exactly like C,
    # even though the plain Python ints passed in would not
    # Attacker: width = 100_000, height = 100_000, bytes_per_pixel = 4
    # True product 4 * 10**13 does not fit in int32
    dims = np.array([width, height, bytes_per_pixel], dtype=np.int32)
    return dims.prod(dtype=np.int32)  # wraps to a small or negative value

Why this is vulnerable: the Python-level reasoning that made the first pattern safe from wraparound stops at the NumPy call. width, height and bytes_per_pixel arrive as arbitrary-precision int objects, but np.array(..., dtype=np.int32) narrows them to 32 bits and the reduction wraps modulo 2^32 exactly as C would. A function like this typically feeds a buffer size or an image dimension into a C extension, so the wrapped value becomes the size of a real allocation on the other side of the boundary - which is the classic overflow this CWE describes, in a language that is usually assumed not to have it.

Note the explicit dtype=np.int32 on the reduction. Without it, prod() widens the accumulator to the platform integer when the array's dtype is narrower, so the same call may or may not wrap depending on platform and NumPy version - which makes "it didn't overflow when I tried it" worthless as evidence. The wraparound is guaranteed when the accumulator is genuinely fixed at the narrow width: an int64 array on a 64-bit platform, an explicit dtype= as above, scalar arithmetic such as np.int32(width) * np.int32(height), or the value being handed to a C extension that expects int32. Of those, only the scalar form announces itself: NumPy 2.5.2 emits RuntimeWarning: overflow encountered in scalar multiply for np.int32(100_000) * np.int32(100_000), while the reduction above wraps to 1345294336 with no warning at all. Do not read a quiet run as a safe one.

Secure Patterns

Validate Range Before Using a Value as a Size

import sys

def allocate_buffer(count: int, size: int) -> bytearray:
    if count < 0 or count > 1_000_000:
        raise ValueError("Invalid count")
    if size < 0 or size > 10_000:
        raise ValueError("Invalid size")

    total = count * size  # Exact regardless of magnitude - Python ints don't overflow

    if total > 100_000_000:  # Application-level practical limit, not a language limit
        raise ValueError("Requested buffer too large")

    return bytearray(total)

Why this works: Python's arbitrary-precision int guarantees count * size is computed exactly - there's no wraparound to check for - but that exactness doesn't bound the result to something the process can actually allocate. Validating both the inputs and the computed total against practical, application-specific limits prevents a memory-exhaustion denial of service that overflow-checking alone wouldn't catch.

Bound Values Before Crossing an FFI/NumPy Boundary

import numpy as np

INT32_MAX = 2**31 - 1

def total_bytes(width: int, height: int, bytes_per_pixel: int) -> int:
    if width < 0 or height < 0 or bytes_per_pixel < 0:
        raise ValueError("Dimensions must not be negative")

    # Compute in plain Python ints first - exact regardless of magnitude
    total = width * height * bytes_per_pixel

    if total > INT32_MAX:
        raise ValueError("Requested dimensions exceed int32 range")

    return total  # Safe to narrow into an int32-typed field/array now

Why this works: The negative check comes first because an upper-bound test alone does not catch a sign error - two negative dimensions multiply to a positive product that passes total > INT32_MAX, and one negative dimension produces a negative total that passes it as well. Once a value crosses into NumPy, a C extension, or ctypes, it's subject to that layer's fixed-width integer rules again - Python's own overflow-free int doesn't extend across the FFI boundary. Doing the calculation in plain Python ints first and validating the result against the target type's actual range before narrowing it prevents the C-side overflow, instead of performing the multiplication directly in the fixed-width dtype where it can already have wrapped.

Testing

  • Very large values (10**12, sys.maxsize, sys.maxsize + 1) to confirm they're rejected by application-level bounds rather than allowed through because "Python ints don't overflow."
  • Values at and just past the target C type's range (2**31 - 1, 2**31, 2**32) when the value crosses into NumPy, ctypes, or a C extension.
  • Negative values, to confirm they're rejected rather than silently accepted (Python's int supports negative values with no special-casing, unlike an unsigned C type).
  • Confirm the failure mode is a controlled ValueError/validation error, not an uncaught MemoryError from an oversized allocation attempt.

Common Pitfalls

  • Assuming "Python ints don't overflow" means "no validation needed": that's true for the arithmetic itself, but a value in the billions is still a valid Python int, and bytearray(), range() or [0] * n will accept it, exhausting memory or CPU well before any "overflow" would occur in a fixed-width language.
  • Validating the Python-side inputs but not the NumPy/C-extension-side result: dtype=np.int32 (or any fixed-width dtype) reintroduces classic wraparound - a value that passed Python-side range checks can still wrap once cast into the array's underlying C type if the checked range doesn't match the dtype's actual range.
  • Relying on sys.maxsize as a universal upper bound: sys.maxsize reflects the platform pointer/index size, not a safe allocation size - an allocation request just under sys.maxsize will still exhaust available memory long before hitting that theoretical ceiling.
  • Catching MemoryError and treating it as recoverable: by the time MemoryError is raised, the process may already be under memory pressure that affects other requests - validate the requested size before attempting the allocation rather than using the exception as the primary control.

Additional Resources