Skip to content

CWE-196: Unsigned to Signed Conversion Error - C

Overview

C converts an unsigned value larger than a signed type's maximum implicitly whenever it is assigned to a signed variable, passed to a parameter of signed type, or cast - there is no compiler error and, without -Wsign-conversion, often no warning. On every mainstream implementation the high bit of the unsigned bit pattern becomes the sign bit, so 0xFFFFFFFF (UINT32_MAX) arrives as -1 in a 32-bit signed int. That outcome is the compiler's choice rather than the language's: C leaves an unrepresentable unsigned-to-signed conversion implementation-defined and even permits an implementation-defined signal. Neither the finding nor the fix depends on which value you get: range-check before converting rather than reason about what the conversion will produce.

A mixed comparison is not one of those conversion sites, which is worth knowing when triaging a finding. The usual arithmetic conversions convert the unsigned operand to the signed type only where that type can represent every value it might hold, and in every other case the signed operand is the one that converts - so a mixed < or > cannot produce this direction, and that finding belongs on CWE-195, where the conversion is defined as reduction modulo 2^N rather than left to the implementation. What a comparison does carry here is an operand somebody has already cast, which is the second pattern below.

The most common source is strlen() or another size_t-returning call being assigned directly into a plain int, which works for ordinary inputs and breaks silently once the input is large enough.

Common Vulnerable Patterns

size_t to int Without Validation

#include <string.h>
#include <stdlib.h>

// VULNERABLE - no range check before the size_t-to-int cast
int get_string_length(const char *str) {
    size_t len = strlen(str);   // unsigned
    return (int)len;             // negative if len > INT_MAX
}

void process_string(const char *input) {
    int length = get_string_length(input);
    if (length < 1000) {                 // negative length passes this check
        char *buffer = malloc(length);   // malloc() with a negative int argument
    }
}

Why this is vulnerable: strlen() returns size_t. On a 64-bit system int is narrower than size_t, so the cast keeps only the low 32 bits and reads them as signed. A length between 2 GB and 4 GB becomes negative, and because malloc() takes size_t that negative int converts straight back to unsigned - a huge value the allocation cannot meet, so it fails. A length of 4 GB or more is the dangerous case: it truncates to a small positive number (4 GB + 5 bytes becomes 5), malloc() succeeds at that size, and the copy that follows still uses the true length.

Unsigned Array Size Cast to Signed

#include <string.h>

// VULNERABLE - both counts cast to int before comparing, so a large count no longer compares correctly
void copy_array(int *dest, size_t dest_count, int *src, size_t src_count) {
    int signed_dest_count = (int)dest_count;
    int signed_src_count = (int)src_count;

    // If either count exceeds INT_MAX, the cast produces the wrong number -
    // negative below 4 GB, a small positive one at or above it - and the
    // comparison no longer reflects the real sizes
    if (signed_src_count <= signed_dest_count) {
        memcpy(dest, src, src_count * sizeof(int));
    }
}

Why this is vulnerable: Converting both counts to int before comparing throws away the guarantee that size_t comparisons are always correct for non-negative values. A src_count between 2 GB and 4 GB becomes negative and a larger one truncates to a small positive value, so either way signed_src_count <= signed_dest_count holds no matter how small dest really is, and the memcpy below it runs on the true, unconverted count - the comparison and the copy are looking at two different numbers.

Secure Patterns

Range-Checked Conversion Helper

#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>

bool safe_size_t_to_int(size_t value, int *result) {
    if (value > INT_MAX) {
        return false;
    }
    *result = (int)value;
    return true;
}

int get_string_length_safe(const char *str) {
    size_t len = strlen(str);
    int result;
    if (!safe_size_t_to_int(len, &result)) {
        return -1;   // explicit error indicator, not a wrapped value
    }
    return result;
}

Why this works: Checking value > INT_MAX while the value is still size_t catches anything that would become negative when cast, before the conversion happens. The helper's bool return forces every call site to handle the "too large" case explicitly instead of silently accepting a wrapped result.

Keep the Comparison Unsigned

#include <string.h>

int copy_array_safe(int *dest, size_t dest_count, int *src, size_t src_count) {
    if (src_count > dest_count) {   // compared as size_t - no conversion, no wraparound
        return -1;
    }
    memcpy(dest, src, src_count * sizeof(int));
    return 0;
}

Why this works: Comparing size_t values directly, without ever converting either one to int, means there is no point at which a large value can flip sign. This is preferable to converting and then validating, because it removes the conversion entirely.

Check the High Bit for Fixed-Width Types

#include <stdbool.h>
#include <stdint.h>

bool safe_uint32_to_int32(uint32_t value, int32_t *result) {
    if (value > INT32_MAX) {   // equivalent to checking the high bit
        return false;
    }
    *result = (int32_t)value;
    return true;
}

Why this works: For a fixed-width type, "fits in the signed range" and "high bit is clear" are the same test. Using INT32_MAX rather than a manual bitmask keeps the check portable and readable.

Testing

  • Unit-test every conversion helper with a normal value, INT_MAX, (size_t)INT_MAX + 1 (the cast matters - INT_MAX + 1 on its own is signed overflow), (size_t)UINT_MAX + 1 (which truncates to zero rather than to a negative number), and SIZE_MAX, asserting only in-range values succeed.
  • If the code path is reachable from untrusted input (uploaded files, network payloads), test with an input crafted to make the unsigned length exceed the destination signed type's maximum.
  • Compile with -Wsign-compare -Wsign-conversion -Wconversion -Werror, so a sign-conversion warning on a security-relevant path fails the build.
  • Run Cppcheck, Clang Static Analyzer, or a commercial SAST tool and confirm no signed/unsigned findings remain on the fixed paths.
  • On MSVC, the equivalent build setting is /W4 /w44365 with warnings treated as errors (/WX). C4365, the signed/unsigned conversion warning, is off by default at every warning level, so /W4 on its own does not report it.

Common Pitfalls

  • Casting size_t results into int "because the API expects int": without a range check first, this just moves the wraparound into whatever code consumes the int next.
  • Testing only with small strings/buffers: a conversion bug that only triggers above INT_MAX elements won't show up in normal unit tests unless a test deliberately constructs an oversized input (or mocks the length).
  • Converting both operands of a comparison to signed: as in the array-size example above, casting both sides to int before comparing can make the comparison itself wrong, even though each individual cast might look locally reasonable.
  • Relying on -Wall -Wextra alone: -Wsign-conversion is not enabled by either of those on most compilers and must be added explicitly, or the conversion compiles without any warning.

Additional Resources