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 + 1on its own is signed overflow),(size_t)UINT_MAX + 1(which truncates to zero rather than to a negative number), andSIZE_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 /w44365with warnings treated as errors (/WX). C4365, the signed/unsigned conversion warning, is off by default at every warning level, so/W4on its own does not report it.
Common Pitfalls
- Casting
size_tresults intoint"because the API expectsint": without a range check first, this just moves the wraparound into whatever code consumes theintnext. - Testing only with small strings/buffers: a conversion bug that only triggers above
INT_MAXelements 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
intbefore comparing can make the comparison itself wrong, even though each individual cast might look locally reasonable. - Relying on
-Wall -Wextraalone:-Wsign-conversionis not enabled by either of those on most compilers and must be added explicitly, or the conversion compiles without any warning.