CWE-196: Unsigned to Signed Conversion Error
Overview
Unsigned to signed conversion errors occur when large unsigned values are cast to signed types, causing them to be interpreted as negative numbers due to sign bit interpretation. This leads to failed comparisons, incorrect arithmetic, negative array indices, and logic errors.
Risk
Medium-High: Converting large unsigned values to signed produces negative numbers (e.g., 0xFFFFFFFF becomes -1), causing failed bounds checks, negative array indices (out-of-bounds access), incorrect loop termination, arithmetic errors, and bypassed validation.
Remediation Steps
Core principle: Avoid unsigned-to-signed conversion surprises; validate ranges before conversion.
Locate Unsigned to Signed Conversions in Your Code
When reviewing security scan results:
- Find large unsigned conversions: Identify where unsigned values (size_t, unsigned int, uint64_t) are cast to signed types
- Check string length operations: Look for strlen() results stored in signed int variables
- Identify array size conversions: Find where array/buffer sizes are converted from size_t to int
- Trace converted values: Determine how converted values are used (loop bounds, array indices, size checks, comparisons)
- Review function signatures: Check where unsigned parameters/returns become signed
Common problematic patterns:
// strlen() to int
size_t len = strlen(str); // Returns size_t (unsigned)
int length = len; // If len > INT_MAX, becomes negative!
// Array size to signed
size_t array_size = 0xFFFFFFFF; // 4GB
int size = array_size; // Becomes -1!
// Large unsigned constant
unsigned int big = 0x80000000; // 2147483648
int signed_val = big; // Becomes -2147483648
// Function parameter conversion
void process(int count); // Expects signed
size_t len = strlen(input);
process(len); // Implicit conversion - could be negative!
Validate Range Before Converting to Signed (Primary Defense)
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
// VULNERABLE - size_t to int without validation
int get_string_length_bad(const char *str) {
size_t len = strlen(str); // Returns size_t (unsigned)
// Attack: if len > INT_MAX (e.g., 3GB string on 64-bit system)
// Cast produces negative number!
return (int)len; // May return negative!
}
void process_string_bad(const char *input) {
int length = get_string_length_bad(input);
// Attack: length is negative (e.g., -2147483648)
// Negative bypasses check!
if (length < 1000) {
char *buffer = malloc(length); // Negative size - undefined!
}
}
// SECURE - validate before conversion
bool safe_size_t_to_int(size_t value, int *result) {
// Check if value fits in int range
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;
// Validate conversion is safe
if (!safe_size_t_to_int(len, &result)) {
fprintf(stderr, "String too long for int\n");
return -1; // Error indicator
}
return result;
}
void process_string_safe(const char *input) {
int length = get_string_length_safe(input);
// Check for error and validate range
if (length < 0 || length >= 1000) {
fprintf(stderr, "Invalid or excessive length\n");
return;
}
char *buffer = malloc(length);
if (buffer != NULL) {
free(buffer);
}
}
// VULNERABLE - array size conversion
void copy_array_bad(int *dest, size_t dest_size, int *src, size_t src_size) {
int signed_dest_size = (int)dest_size;
int signed_src_size = (int)src_size;
// If sizes > INT_MAX, these become negative
// Comparison fails: -1 <= -2 might pass when it shouldn't!
if (signed_src_size <= signed_dest_size) {
memcpy(dest, src, src_size); // Potential overflow
}
}
// SECURE - use size_t throughout
int copy_array_safe(int *dest, size_t dest_size, int *src, size_t src_size) {
// Work with size_t - no conversion
if (src_size > dest_size) {
fprintf(stderr, "Source larger than destination\n");
return -1;
}
memcpy(dest, src, src_size * sizeof(int));
return 0;
}
Why this works: Large unsigned values have the high bit set, which is interpreted as the sign bit in signed types, producing negative numbers. Always validate value <= INT_MAX (or appropriate MAX constant) before conversion.
Use Consistent Types Throughout
#include <stddef.h>
#include <sys/types.h>
// VULNERABLE - unnecessary conversion
void process_file_bad(const char *filename) {
FILE *f = fopen(filename, "rb");
fseek(f, 0, SEEK_END);
long file_size = ftell(f); // Returns long
int size = file_size; // Unnecessary narrowing
// If file > 2GB, size becomes negative
if (size > 0) {
char *buffer = malloc(size); // Wrong!
}
}
// SECURE - use appropriate types
void process_file_safe(const char *filename) {
FILE *f = fopen(filename, "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fclose(f);
// Validate file size
if (file_size < 0 || file_size > 100000000) {
fprintf(stderr, "Invalid file size\n");
return;
}
// Use size_t for allocation
size_t alloc_size = (size_t)file_size;
char *buffer = malloc(alloc_size);
if (buffer != NULL) {
free(buffer);
}
}
// Use ssize_t for signed sizes (POSIX)
ssize_t get_buffer_position(size_t current, int offset) {
// ssize_t can represent both size_t values and negative errors
if (current > SSIZE_MAX) {
return -1; // Too large
}
ssize_t pos = (ssize_t)current + offset;
if (pos < 0) {
return -1; // Underflow
}
return pos;
}
Check for High Bit Before Conversion
#include <stdint.h>
#include <limits.h>
// Check if high bit is set (indicates > INT_MAX)
bool is_safe_for_int(uint32_t value) {
// If high bit is set, value > INT_MAX
return (value & 0x80000000) == 0;
}
// Validate against MAX constant
bool safe_uint32_to_int32(uint32_t value, int32_t *result) {
if (value > INT32_MAX) {
return false;
}
*result = (int32_t)value;
return true;
}
// For size_t to ssize_t
bool safe_size_t_to_ssize_t(size_t value, ssize_t *result) {
if (value > SSIZE_MAX) {
return false;
}
*result = (ssize_t)value;
return true;
}
void process_data_safe(size_t data_size) {
ssize_t signed_size;
// Validate conversion
if (!safe_size_t_to_ssize_t(data_size, &signed_size)) {
fprintf(stderr, "Data size too large\n");
return;
}
// Safe to use signed_size
printf("Processing %zd bytes\n", signed_size);
}
Enable Compiler Warnings and Static Analysis
# GCC/Clang - enable conversion warnings
gcc -Wsign-compare -Wsign-conversion -Wconversion -Werror \
-Wall -Wextra -o program program.c
# Clang - additional checks
clang -Wsign-compare -Wsign-conversion -Wconversion \
-Wimplicit-int-conversion -Werror -o program program.c
# MSVC - all warnings
cl /W4 /WX program.c
Common warnings:
// warning: implicit conversion changes signedness
size_t len = strlen(str);
int length = len; // Warning!
// warning: comparison of integers of different signs
unsigned int u = 10;
int s = -5;
if (u > s) {} // Warning!
Static analysis tools:
- Veracode Static Analysis
- Coverity
- Clang Static Analyzer
- Cppcheck
- PVS-Studio
Test with Large Values and Edge Cases
#include <assert.h>
#include <limits.h>
void test_unsigned_to_signed_conversion() {
int result;
// Test 1: Small value - should succeed
assert(safe_size_t_to_int(100, &result) == true);
assert(result == 100);
printf("✓ Test 1: Small value\n");
// Test 2: INT_MAX - should succeed
assert(safe_size_t_to_int(INT_MAX, &result) == true);
assert(result == INT_MAX);
printf("✓ Test 2: INT_MAX\n");
// Test 3: INT_MAX + 1 - should fail
assert(safe_size_t_to_int((size_t)INT_MAX + 1, &result) == false);
printf("✓ Test 3: Overflow detection\n");
// Test 4: SIZE_MAX - should fail
assert(safe_size_t_to_int(SIZE_MAX, &result) == false);
printf("✓ Test 4: SIZE_MAX rejection\n");
// Test 5: High bit set (0x80000000 for 32-bit)
assert(safe_size_t_to_int(0x80000000, &result) == false);
printf("✓ Test 5: High bit rejection\n");
}
void test_large_string_length() {
// Simulate large string (can't actually allocate)
size_t large_len = (size_t)INT_MAX + 1000;
int result;
bool success = safe_size_t_to_int(large_len, &result);
assert(success == false);
printf("✓ Large string length rejected\n");
}
int main() {
test_unsigned_to_signed_conversion();
test_large_string_length();
printf("All unsigned to signed conversion tests passed!\n");
return 0;
}
Common Vulnerable Patterns
size_t to int Without Validation
#include <string.h>
#include <stdlib.h>
// Dangerous: size_t to int conversion
int get_string_length(const char *str) {
size_t len = strlen(str); // Returns size_t (unsigned)
// Attack: if len > INT_MAX (e.g., 3GB string)
// Cast produces negative number
return (int)len; // May return negative!
}
void process_string(const char *input) {
int length = get_string_length(input);
// Negative length bypasses check!
if (length < 1000) {
char *buffer = malloc(length); // Negative size!
}
}
Unsigned Array Size Cast to Signed
#include <string.h>
#include <stdlib.h>
// Dangerous: unsigned array size to signed
void copy_array(int *dest, size_t dest_size, int *src, size_t src_size) {
int signed_dest_size = (int)dest_size;
int signed_src_size = (int)src_size;
// If sizes > INT_MAX, these become negative
if (signed_src_size <= signed_dest_size) { // Check fails!
memcpy(dest, src, src_size);
}
}
## Secure Patterns
### Range-Checked Conversion Helper (C)
```c
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <stdbool.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; // Error: too long
}
return result;
}
void process_string_safe(const char *input) {
int length = get_string_length_safe(input);
if (length < 0 || length >= 1000) {
return; // Invalid or too long
}
char *buffer = malloc(length);
}
Why this works: Checking value > INT_MAX before conversion prevents values with the high bit set (which would be interpreted as negative in signed representation) from being cast. Values larger than INT_MAX will have the sign bit set when cast to signed, becoming negative.
Consistent Type Usage (C)
#include <string.h>
int copy_array_safe(int *dest, size_t dest_size,
int *src, size_t src_size) {
// Work with size_t throughout
if (src_size > dest_size) {
return -1;
}
memcpy(dest, src, src_size * sizeof(int));
return 0;
}
Why this works: Using size_t throughout avoids the need to convert to signed types. Size comparisons remain accurate, and there's no risk of large values becoming negative through type conversion.
Exception-Based Validation (C++)
#include <limits>
#include <stdexcept>
#include <string>
class SafeConversion {
public:
static int sizeToInt(size_t value) {
if (value > static_cast<size_t>(std::numeric_limits<int>::max())) {
throw std::overflow_error("Size too large for int");
}
return static_cast<int>(value);
}
static int getStringLength(const std::string& str) {
return sizeToInt(str.length());
}
// Better: use size_t throughout
static size_t getLength(const std::string& str) {
return str.length(); // Keep as size_t
}
};
Why this works: Using std::numeric_limits<int>::max() provides a portable way to check if the unsigned value fits in a signed int. Throwing exceptions on overflow prevents silent failures and forces error handling.
Type-Safe APIs (Java)
public class SafeConversion {
public static int safeLongToInt(long value) {
if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Value out of int range: " + value);
}
return (int) value;
}
public static int getArrayLength(byte[] array) {
// Java arrays use int for length - already safe
return array.length;
}
// For very large collections
public static int getCollectionSize(java.util.Collection<?> coll) {
int size = coll.size();
// size() returns int, but validate anyway
if (size < 0) {
throw new IllegalStateException("Invalid collection size");
}
return size;
}
}
Why this works: Java uses signed integers for all array and collection sizes, eliminating unsigned-to-signed conversion issues. Validating bounds ensures values remain within expected ranges, and the type system prevents most conversion errors at compile time.
Security Checklist
- All unsigned to signed conversions validate value <= INT_MAX (or SSIZE_MAX)
- strlen() results not blindly cast to int
- Array sizes use size_t throughout (no conversion to int)
- High bit checked for 32-bit conversions
- Compiler warnings enabled (-Wsign-conversion, -Wconversion)
- Tests cover: normal values, INT_MAX, INT_MAX+1, SIZE_MAX, high bit set