CWE-195: Signed to Unsigned Conversion Error
Overview
Signed to unsigned conversion errors occur when negative signed integers are cast to unsigned types, resulting in large positive values due to two's complement representation. This causes buffer overflows, incorrect size calculations, bypassed validation, and logic errors.
Risk
High: Converting negative signed values to unsigned produces huge positive numbers (e.g., -1 becomes 4294967295), bypassing size checks, causing buffer overflows when used for allocation/indexing, infinite loops, and memory corruption. Common in return value checks and size calculations.
Remediation Steps
Core principle: Avoid signed/unsigned confusion; cast deliberately and validate before comparisons or indexing.
Locate Signed to Unsigned Conversions in Your Code
When reviewing security scan results:
- Find negative value conversions: Identify where signed integers (especially function return values) are cast to unsigned types
- Check error handling: Look for conversions of -1, -errno, or other negative error indicators to size_t or unsigned types
- Identify comparison issues: Find signed/unsigned comparisons that could bypass security checks
- Trace data flow: Determine where converted values are used (buffer sizes, array indices, loop bounds, memory allocations)
- Review function returns: Check read(), recv(), snprintf(), and other functions that return -1 on error
Common problematic patterns:
// Casting error return to unsigned
ssize_t bytes = read(fd, buf, size); // Returns -1 on error
size_t count = (size_t)bytes; // -1 becomes SIZE_MAX!
// Signed/unsigned comparison
int offset = -1; // User-controlled or error value
size_t buf_size = 1024;
if (offset < buf_size) { // -1 converted to SIZE_MAX - check fails!
buf[offset] = x; // Out of bounds!
}
// Using signed offset with unsigned size
int user_offset = get_user_input(); // Could be negative
size_t index = (size_t)user_offset; // Negative becomes huge
malloc(index); // Huge allocation or failure
Check for Negative Before Converting to Unsigned (Primary Defense)
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
// VULNERABLE - no negative check before conversion
void read_file_data_bad(int fd) {
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
// Attack: read() returns -1 on error
// Cast to size_t: -1 becomes SIZE_MAX (eg 18446744073709551615 on 64-bit)
size_t count = (size_t)bytes_read;
// This check is bypassed - SIZE_MAX > 0!
if (count > 0) {
char *data = malloc(count); // Huge allocation or may return NULL
memcpy(data, buffer, count); // Crash if data is NULL, or massive buffer overread
}
}
// SECURE - validate non-negative before conversion
void read_file_data_safe(int fd) {
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
// Check for error first (negative return)
if (bytes_read < 0) {
perror("read failed");
return;
}
// Now safe to convert to size_t
size_t count = (size_t)bytes_read;
if (count > 0 && count <= sizeof(buffer)) {
char *data = malloc(count);
if (data != NULL) {
memcpy(data, buffer, count);
free(data);
}
}
}
// VULNERABLE - signed/unsigned comparison
int process_buffer_bad(char *buf, int user_offset) {
size_t buf_size = 1024;
// Attack: user_offset = -1
// Comparison: -1 < 1024 (signed vs unsigned)
// -1 is implicitly converted to unsigned SIZE_MAX
// SIZE_MAX >= 1024, so check PASSES - BYPASSED!
if (user_offset < buf_size) {
return buf[user_offset]; // Out of bounds access!
}
return 0;
}
// SECURE - check negative before comparison
int process_buffer_safe(char *buf, size_t buf_size, int user_offset) {
// Validate signed value is non-negative first
if (user_offset < 0) {
fprintf(stderr, "Negative offset not allowed\n");
return -1;
}
// Safe to convert to size_t
size_t offset = (size_t)user_offset;
// Now valid comparison
if (offset >= buf_size) {
fprintf(stderr, "Offset out of bounds\n");
return -1;
}
return buf[offset];
}
// Helper function for safe conversion
#include <stdbool.h>
bool safe_int_to_size_t(int value, size_t *result) {
// Reject negative values
if (value < 0) {
return false;
}
*result = (size_t)value;
return true;
}
void allocate_buffer_safe(int requested_size) {
size_t alloc_size;
// Validate conversion is safe
if (!safe_int_to_size_t(requested_size, &alloc_size)) {
fprintf(stderr, "Invalid size: negative\n");
return;
}
// Additional validation
if (alloc_size > 10000000) {
fprintf(stderr, "Size too large\n");
return;
}
char *buffer = malloc(alloc_size);
if (buffer != NULL) {
free(buffer);
}
}
Why this works: Negative signed values become huge positive values when converted to unsigned due to two's complement representation. Always validate >= 0 before conversion.
Use Consistent Types to Avoid Mixed Signed/Unsigned Operations
#include <unistd.h>
#include <sys/types.h>
// VULNERABLE - mixing signed and unsigned
void copy_data_bad(char *dest, int dest_size, char *src) {
size_t src_len = strlen(src); // Returns size_t (unsigned)
// DANGEROUS: comparing unsigned with signed
// If dest_size is negative, it gets promoted to huge unsigned!
if (src_len < dest_size) {
memcpy(dest, src, src_len);
}
}
// SECURE - use consistent types
void copy_data_safe(char *dest, size_t dest_size, char *src) {
size_t src_len = strlen(src);
// Both size_t - safe unsigned comparison
if (src_len >= dest_size) {
fprintf(stderr, "Source too large\n");
return;
}
memcpy(dest, src, src_len);
dest[src_len] = '\0';
}
// Use ssize_t for signed sizes (POSIX)
int read_into_buffer(int fd, char *dest, size_t dest_size) {
ssize_t bytes = read(fd, dest, dest_size); // ssize_t can be negative
if (bytes < 0) {
return -1; // Error
}
// Safe to convert ssize_t to size_t after validation
size_t bytes_read = (size_t)bytes;
return (bytes_read <= INT_MAX) ? (int)bytes_read : -1;
}
// Comparison pitfall demonstration
void demonstrate_comparison_issue() {
unsigned int u = 10;
int s = -5;
// WRONG: s is implicitly promoted to unsigned!
// -5 becomes 4294967291 (huge positive)
if (s < u) {
printf("This won't print!\n");
} else {
printf("Comparison failed: -5 > 10!\n");
}
// CORRECT: check for negative first
if (s < 0 || (unsigned int)s < u) {
printf("Safe comparison works\n");
}
}
Validate All Function Return Values Before Use
#include <stdio.h>
#include <unistd.h>
// VULNERABLE - using error return as size
void format_and_allocate_bad(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
// snprintf returns -1 on encoding error
int len = vsnprintf(NULL, 0, fmt, args);
va_end(args);
// Attack: len = -1 (encoding error)
size_t buf_size = (size_t)len + 1; // -1 + 1 = SIZE_MAX!
char *buffer = malloc(buf_size); // Huge allocation
}
// SECURE - validate return value
void format_and_allocate_safe(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
int len = vsnprintf(NULL, 0, fmt, args);
va_end(args);
// Check for error
if (len < 0) {
fprintf(stderr, "Formatting error\n");
return;
}
// Safe conversion after validation
size_t buf_size = (size_t)len + 1;
if (buf_size > 10000) {
fprintf(stderr, "Format result too large\n");
return;
}
char *buffer = malloc(buf_size);
if (buffer != NULL) {
va_start(args, fmt);
vsnprintf(buffer, buf_size, fmt, args);
va_end(args);
free(buffer);
}
}
// recv() validation
ssize_t receive_data_safe(int sock, char *buffer, size_t size) {
ssize_t received = recv(sock, buffer, size, 0);
// Check for error (-1) or connection closed (0)
if (received <= 0) {
if (received < 0) {
perror("recv failed");
}
return -1;
}
// Safe to use as size_t now
return received;
}
Enable Compiler Warnings and Use Static Analysis
# GCC/Clang - enable signed/unsigned warnings
gcc -Wsign-compare -Wsign-conversion -Wconversion -Werror \
-Wall -Wextra -o program program.c
# Clang - more strict
clang -Wsign-compare -Wsign-conversion -Wconversion \
-Wimplicit-int-conversion -Werror -o program program.c
# MSVC - enable all warnings
cl /W4 /WX program.c
Common warnings to watch for:
// warning: comparison of integers of different signs
int a = -1;
unsigned int b = 10;
if (a < b) {} // Warning!
// warning: implicit conversion changes signedness
ssize_t bytes = read(fd, buf, size);
size_t count = bytes; // Warning!
// warning: operand of ?: changes signedness
size_t result = (error) ? -1 : count; // Warning!
Static analysis tools:
- Coverity
- Clang Static Analyzer
- Veracode Static Analysis
- Cppcheck with
--enable=warning - PVS-Studio
Test with Negative Values and Edge Cases
#include <assert.h>
#include <limits.h>
void test_safe_conversion() {
size_t result;
// Test 1: Positive value - should succeed
assert(safe_int_to_size_t(100, &result) == true);
assert(result == 100);
printf("✓ Test 1: Positive value\n");
// Test 2: Negative value - should fail
assert(safe_int_to_size_t(-1, &result) == false);
printf("✓ Test 2: Negative rejection\n");
// Test 3: Zero - should succeed
assert(safe_int_to_size_t(0, &result) == true);
assert(result == 0);
printf("✓ Test 3: Zero\n");
// Test 4: INT_MAX - should succeed
assert(safe_int_to_size_t(INT_MAX, &result) == true);
assert(result == (size_t)INT_MAX);
printf("✓ Test 4: INT_MAX\n");
// Test 5: INT_MIN - should fail
assert(safe_int_to_size_t(INT_MIN, &result) == false);
printf("✓ Test 5: INT_MIN rejection\n");
}
void test_signed_unsigned_comparison() {
// Test that negative values don't bypass checks
int offset = -1;
size_t size = 1024;
// Unsafe comparison (for demonstration)
int unsafe_result = (offset < size); // TRUE (wrong!)
// Safe comparison
int safe_result = (offset >= 0 && (size_t)offset < size); // FALSE (correct)
printf("Unsafe: %d, Safe: %d\n", unsafe_result, safe_result);
assert(safe_result == 0); // Should be false
}
int main() {
test_safe_conversion();
test_signed_unsigned_comparison();
printf("All signed to unsigned conversion tests passed!\n");
return 0;
}
Common Vulnerable Patterns
Error Return Value Cast to Unsigned
#include <unistd.h>
#include <stdlib.h>
// Dangerous: read() returns -1 on error
void read_file_data(int fd) {
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
// Attack: read() returns -1 (error)
// Cast to size_t: -1 becomes SIZE_MAX (huge number)
size_t count = (size_t)bytes_read;
// This check is useless - SIZE_MAX > 0!
if (count > 0) {
char *data = malloc(count); // Huge allocation!
memcpy(data, buffer, count); // Buffer overread!
}
}
Signed/Unsigned Comparison Bypass
// Dangerous: signed/unsigned comparison
int process_buffer(char *buf, int user_offset) {
size_t buf_size = 1024;
// Attack: user_offset = -1
// Comparison: -1 < 1024 (signed vs unsigned)
// -1 is converted to unsigned -> SIZE_MAX
// SIZE_MAX >= 1024, so check passes!
if (user_offset < buf_size) {
return buf[user_offset]; // Out of bounds!
}
}
Negative Value in Allocation Size
#include <stdlib.h>
// Dangerous: negative to unsigned in allocation
void allocate_sized_buffer(int requested_size) {
// Attack: requested_size = -1
size_t alloc_size = (size_t)requested_size; // Becomes SIZE_MAX
char *buffer = malloc(alloc_size); // Huge allocation or fail
}
## Secure Patterns
### Validate Negative Before Conversion (C)
```c
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
void read_file_data_safe(int fd) {
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
// Check for error first
if (bytes_read < 0) {
perror("read failed");
return;
}
// Now safe to convert to size_t
size_t count = (size_t)bytes_read;
if (count > 0 && count <= sizeof(buffer)) {
char *data = malloc(count);
if (data != NULL) {
memcpy(data, buffer, count);
}
}
}
Why this works: Checking bytes_read < 0 before converting to size_t prevents negative error values (like -1) from wrapping to huge positive values (SIZE_MAX). This ensures only valid, non-negative values are used as unsigned sizes.
Pre-Validation for Signed Offsets (C)
int process_buffer_safe(char *buf, size_t buf_size, int user_offset) {
// Validate signed value before using
if (user_offset < 0) {
return -1; // Invalid offset
}
// Now safe to convert
size_t offset = (size_t)user_offset;
if (offset >= buf_size) {
return -1; // Out of bounds
}
return buf[offset];
}
Why this works: Checking user_offset < 0 before conversion prevents negative values from wrapping to large unsigned values. In signed/unsigned comparisons, negative values get implicitly promoted to unsigned, bypassing bounds checks.
Range Validation for Allocation Sizes (C)
#include <stdio.h>
#include <stdlib.h>
void allocate_sized_buffer_safe(int requested_size) {
// Validate before conversion
if (requested_size < 0 || requested_size > 10000000) {
fprintf(stderr, "Invalid size\n");
return;
}
size_t alloc_size = (size_t)requested_size;
char *buffer = malloc(alloc_size);
}
Why this works: Validating both the lower bound (< 0) and upper bound prevents negative values from becoming huge unsigned values and also enforces practical allocation limits.
Safe Conversion Helper Function (C)
#include <stdbool.h>
#include <stddef.h>
#include <limits.h>
bool safe_int_to_size_t(int value, size_t *result) {
if (value < 0) {
return false;
}
*result = (size_t)value;
return true;
}
int read_into_buffer(int fd, char *dest, int dest_size) {
size_t safe_size;
// Validate dest_size is non-negative
if (!safe_int_to_size_t(dest_size, &safe_size)) {
errno = EINVAL;
return -1;
}
ssize_t bytes = read(fd, dest, safe_size);
if (bytes < 0) {
return -1; // Read error
}
// Safe conversion - we know bytes >= 0
return (int)bytes;
}
Why this works: Centralized validation in a helper function ensures consistent checking across the codebase. The boolean return makes it clear whether conversion succeeded, preventing accidental use of invalid values.
Automatic Bounds Checking (Java)
import java.io.*;
public class SafeConversion {
public static long safeIntToLong(int value) {
// Java int is always signed, long can hold it
return (long) value; // Safe, no data loss
}
public static int safeCharArrayAccess(char[] buffer, int offset) {
// Java checks bounds automatically
if (offset < 0 || offset >= buffer.length) {
throw new ArrayIndexOutOfBoundsException("Invalid offset");
}
return buffer[offset];
}
public static byte[] readExactly(InputStream in, int size)
throws IOException {
if (size < 0 || size > 10_000_000) {
throw new IllegalArgumentException("Invalid size");
}
byte[] buffer = new byte[size];
int offset = 0;
while (offset < size) {
int bytesRead = in.read(buffer, offset, size - offset);
if (bytesRead < 0) {
throw new EOFException("Stream ended early");
}
offset += bytesRead;
}
return buffer;
}
}
Why this works: Java uses signed integers throughout and throws exceptions on invalid operations. Explicit validation for negative values and bounds checking prevents signed-to-unsigned conversion errors that don't exist in Java's type system.
Explicit Validation (Python)
import os
def read_file_data(fd: int, max_size: int) -> bytes:
"""Read from file descriptor with validation."""
if max_size < 0:
raise ValueError('Size cannot be negative')
if max_size > 10_000_000:
raise ValueError('Size too large')
# Python os.read returns bytes object
try:
data = os.read(fd, max_size)
return data
except OSError as e:
# Handle error
raise
def process_buffer(buffer: bytes, offset: int) -> int:
"""Access buffer at offset with validation."""
if offset < 0:
raise IndexError('Offset cannot be negative')
if offset >= len(buffer):
raise IndexError('Offset out of bounds')
return buffer[offset]
Why this works: Python doesn't have unsigned integers, avoiding this entire class of errors. Explicit validation ensures values are within expected ranges, and Python's automatic bounds checking prevents out-of-range access.
Security Checklist
- All signed to unsigned conversions check for negative first
- Function return values (read, recv, snprintf) validated before use
- No signed/unsigned comparisons without negative check
- Compiler warnings enabled (-Wsign-compare, -Wsign-conversion)
- Static analysis tools report no signed/unsigned issues
- Tests cover: positive values, -1, INT_MIN, zero, INT_MAX
- Error paths (negative returns) properly handled