CWE-197: Numeric Truncation Error
Overview
Numeric truncation occurs when assigning larger numeric types to smaller ones (long to int, double to float, int64 to int32), causing high-order bits or precision to be lost. This leads to incorrect values, buffer overflows when used for sizes, logic errors, and unexpected behavior.
Risk
Medium-High: Truncation causes data loss (value 0x100000001 becomes 1), incorrect buffer allocations, failed range checks, precision loss in calculations, and unexpected wraparound. Dangerous when truncated values are used for memory operations or security checks.
Remediation Steps
Core principle: Avoid truncation by validating values fit in destination types without loss.
Locate Numeric Truncation in Your Code
When reviewing security scan results:
- Find narrowing conversions: Identify where larger types are assigned to smaller types (long→int, int64→int32, double→float, double→int)
- Check implicit conversions: Look for assignments without explicit casts that lose data
- Identify truncation points: Find where high-order bits or decimal precision are lost
- Trace truncated values: Determine where converted values are used (buffer sizes, array indices, calculations, security checks)
- Review time handling: Check for time_t (64-bit) stored in int (32-bit) - Y2038 bug
Common problematic patterns:
// 64-bit to 32-bit truncation
int64_t big = 0x100000001; // 4GB + 1
int32_t small = big; // Truncates to 1
// double to int truncation
double value = 2147483648.5; // > INT_MAX
int result = value; // Undefined behavior!
// size_t to int on 64-bit
size_t len = 0xFFFFFFFF; // 4GB
int size = len; // Becomes -1 on 64-bit!
// Y2038 bug - time_t truncation
time_t future = 2147483648; // Year 2038+
int timestamp = future; // Truncates or overflows
Validate Range Before Narrowing Conversion (Primary Defense)
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
// VULNERABLE - 64-bit to 32-bit without validation
void allocate_from_file_size_bad(int64_t file_size) {
// Attack: file_size = 0x100000001 (4GB + 1 byte)
// Truncation: only lower 32 bits kept = 0x00000001 = 1
int32_t buffer_size = file_size; // Truncates to 1!
// Allocates only 1 byte instead of 4GB!
char *buffer = malloc(buffer_size);
// Attacker writes 4GB into 1-byte buffer = massive overflow
}
// SECURE - validate before narrowing
bool safe_int64_to_int32(int64_t value, int32_t *result) {
// Check if value fits in int32 range
if (value < INT32_MIN || value > INT32_MAX) {
return false;
}
*result = (int32_t)value;
return true;
}
void allocate_from_file_size_safe(int64_t file_size) {
int32_t buffer_size;
// Validate before conversion
if (!safe_int64_to_int32(file_size, &buffer_size)) {
fprintf(stderr, "File size %lld too large for int32\n",
(long long)file_size);
return;
}
// Additional validation
if (buffer_size <= 0 || buffer_size > 10000000) {
fprintf(stderr, "Invalid buffer size: %d\n", buffer_size);
return;
}
char *buffer = malloc(buffer_size);
if (buffer != NULL) {
free(buffer);
}
}
// VULNERABLE - double to int truncation
void process_calculation_bad(double user_value) {
// Attack: user_value = 2147483648.5 (> INT_MAX)
// Conversion to int = UNDEFINED BEHAVIOR!
int result = user_value;
if (result > 0) {
char buffer[result]; // Wrong size or crash
}
}
// SECURE - validate range before truncation
void process_calculation_safe(double user_value) {
// Validate range before truncation
if (user_value < 0 || user_value > INT_MAX) {
fprintf(stderr, "Value %f out of int range\n", user_value);
return;
}
// Check for NaN and infinity
if (isnan(user_value) || isinf(user_value)) {
fprintf(stderr, "Invalid floating point value\n");
return;
}
int result = (int)user_value;
if (result > 0 && result < 10000) {
char *buffer = malloc(result);
if (buffer != NULL) {
free(buffer);
}
}
}
Why this works: Truncation discards high-order bits. Value 0x100000001 loses upper 32 bits, becoming 0x00000001 = 1. Always validate value fits in target type's MIN/MAX range before narrowing.
Fix Y2038 Bug (Time Truncation)
#include <time.h>
#include <stdint.h>
// VULNERABLE - Y2038 bug
struct Event_Bad {
int timestamp; // 32-bit signed: max = 2147483647 = Jan 19, 2038
};
void store_event_bad(time_t event_time) {
struct Event_Bad evt;
// After Jan 19, 2038, time_t > INT32_MAX
evt.timestamp = event_time; // Truncation or overflow!
}
// SECURE - use 64-bit timestamps
struct Event_Safe {
int64_t timestamp; // 64-bit: max = year 292 billion
};
void store_event_safe(time_t event_time) {
struct Event_Safe evt;
// Safe: time_t typically 64-bit, int64_t can hold it
evt.timestamp = (int64_t)event_time;
}
// Conversion helper for legacy code
bool safe_time_t_to_int32(time_t value, int32_t *result) {
if (value < 0 || value > INT32_MAX) {
return false; // Out of range or Y2038 issue
}
*result = (int32_t)value;
return true;
}
Use Explicit Conversions with Comments
#include <stdint.h>
void process_packet(uint64_t packet_size) {
// WRONG: Implicit conversion - easy to miss truncation
uint32_t size = packet_size;
// CORRECT: Explicit cast with validation and comment
uint32_t size_checked;
// Validate: packet size must fit in 32-bit for protocol
if (packet_size > UINT32_MAX) {
fprintf(stderr, "Packet too large: %llu\n",
(unsigned long long)packet_size);
return;
}
// Safe narrowing: validated to fit in uint32_t
size_checked = (uint32_t)packet_size;
}
// C++ - use static_cast to show intentional conversion
#ifdef __cplusplus
void process_packet_cpp(uint64_t packet_size) {
if (packet_size > UINT32_MAX) {
throw std::overflow_error("Packet too large");
}
// Explicit static_cast shows intentional conversion
uint32_t size = static_cast<uint32_t>(packet_size);
}
#endif
Preserve Precision in Calculations
#include <math.h>
// VULNERABLE - premature truncation loses precision
double calculate_average_bad(int total, int count) {
// Integer division truncates: 7/2 = 3, not 3.5
return total / count; // Wrong!
}
// SECURE - preserve precision
double calculate_average_safe(int total, int count) {
if (count == 0) return 0.0;
// Cast to double before division
return (double)total / (double)count;
}
// VULNERABLE - float loses precision
float financial_calculation_bad(float amount, float rate) {
// float has ~7 decimal digits precision
// For money, this loses cents!
return amount * rate;
}
// SECURE - use double for intermediate calculations
double financial_calculation_safe(double amount, double rate) {
// double has ~15 decimal digits precision
return amount * rate;
}
// Better: use integer cents for exact financial calculations
int64_t calculate_price_cents(int64_t amount_cents, int64_t rate_basis_points) {
// Work in cents/basis points - no floating point errors
return (amount_cents * rate_basis_points) / 10000;
}
Enable Compiler Warnings and Test Thoroughly
# GCC/Clang - enable truncation warnings
gcc -Wconversion -Wnarrowing -Wfloat-conversion -Werror \
-Wall -Wextra -o program program.c
# C++ - narrowing in initialization is error
g++ -Wconversion -Wnarrowing -std=c++11 -Werror program.cpp
# Clang - additional warnings
clang -Wconversion -Wshorten-64-to-32 -Wimplicit-int-conversion \
-Werror -o program program.c
# MSVC - all warnings
cl /W4 /WX program.c
Test with edge cases:
#include <assert.h>
void test_truncation_prevention() {
int32_t result;
// Test 1: Value within range
assert(safe_int64_to_int32(100LL, &result) == true);
assert(result == 100);
printf("✓ Test 1: Normal value\n");
// Test 2: INT32_MAX - should succeed
assert(safe_int64_to_int32(INT32_MAX, &result) == true);
assert(result == INT32_MAX);
printf("✓ Test 2: INT32_MAX\n");
// Test 3: INT32_MAX + 1 - should fail
assert(safe_int64_to_int32((int64_t)INT32_MAX + 1, &result) == false);
printf("✓ Test 3: Overflow detection\n");
// Test 4: Truncation scenario - 0x100000001
assert(safe_int64_to_int32(0x100000001LL, &result) == false);
printf("✓ Test 4: High bits set rejection\n");
// Test 5: INT32_MIN - should succeed
assert(safe_int64_to_int32(INT32_MIN, &result) == true);
assert(result == INT32_MIN);
printf("✓ Test 5: INT32_MIN\n");
// Test 6: Below INT32_MIN - should fail
assert(safe_int64_to_int32((int64_t)INT32_MIN - 1, &result) == false);
printf("✓ Test 6: Underflow detection\n");
}
void test_y2038() {
int32_t result;
time_t future = 2147483648; // Jan 19, 2038 + 1 second
// Should fail for 32-bit storage
assert(safe_time_t_to_int32(future, &result) == false);
printf("✓ Y2038 overflow detected\n");
}
int main() {
test_truncation_prevention();
test_y2038();
printf("All numeric truncation tests passed!\n");
return 0;
}
Common Vulnerable Patterns
64-bit to 32-bit Truncation in Allocation
#include <stdlib.h>
#include <stdint.h>
// Dangerous: 64-bit to 32-bit truncation
void allocate_from_file_size(int64_t file_size) {
// Attack: file_size = 0x100000001 (4GB + 1 byte)
int32_t buffer_size = file_size; // Truncates to 1
char *buffer = malloc(buffer_size); // Only 1 byte!
}
Double to Int Truncation
#include <math.h>
// Dangerous: double to int truncation
void process_calculation(double user_value) {
// User provides: 2147483648.5 (> INT_MAX)
int result = user_value; // Undefined behavior!
if (result > 0) {
char buffer[result]; // Wrong size!
}
}
time_t Truncation (Y2038 Bug)
#include <time.h>
// Dangerous: time_t truncation (Y2038 bug)
struct Event {
int timestamp; // 32-bit, overflows in 2038
};
void store_event(time_t event_time) {
struct Event evt;
evt.timestamp = event_time; // Truncation!
}
## Secure Patterns
### Range-Validated Narrowing Conversion (C)
```c
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>
#include <stdbool.h>
#include <time.h>
bool safe_int64_to_int32(int64_t value, int32_t *result) {
if (value < INT32_MIN || value > INT32_MAX) {
return false;
}
*result = (int32_t)value;
return true;
}
void allocate_from_file_size_safe(int64_t file_size) {
int32_t buffer_size;
// Validate before conversion
if (!safe_int64_to_int32(file_size, &buffer_size)) {
fprintf(stderr, "File too large\n");
return;
}
if (buffer_size <= 0 || buffer_size > 10000000) {
fprintf(stderr, "Invalid buffer size\n");
return;
}
char *buffer = malloc(buffer_size);
}
Why this works: Checking value < INT32_MIN || value > INT32_MAX ensures all bits of the value fit within the 32-bit signed range. Truncation occurs when high-order bits are non-zero; this check catches those cases before they cause data loss.
Validated Float-to-Int Conversion (C)
#include <math.h>
#include <limits.h>
void process_calculation_safe(double user_value) {
// Validate range before truncation
if (user_value < 0 || user_value > INT_MAX) {
fprintf(stderr, "Value out of range\n");
return;
}
int result = (int)user_value;
if (result > 0 && result < 10000) {
char *buffer = malloc(result);
}
}
Why this works: Converting floating-point values outside the target integer range causes undefined behavior. Validating that the value fits within INT_MAX and checking for NaN/infinity prevents this. The truncation still loses decimal precision, but the integer part is preserved correctly.
64-bit Timestamp Storage (C)
#include <stdint.h>
#include <time.h>
// Correct: use 64-bit for timestamps
struct Event {
int64_t timestamp; // Won't overflow until year 292 billion
};
void store_event_safe(time_t event_time) {
struct Event evt;
evt.timestamp = (int64_t)event_time; // Safe expansion
}
Why this works: The Y2038 bug occurs because 32-bit signed integers can only represent dates up to January 19, 2038. Using int64_t for timestamps extends the range to approximately 292 billion years, eliminating this overflow issue.
Template-Based Safe Casting (C++)
#include <limits>
#include <stdexcept>
#include <cstdint>
template<typename Target, typename Source>
Target safe_narrow_cast(Source value) {
if (value < std::numeric_limits<Target>::min() ||
value > std::numeric_limits<Target>::max()) {
throw std::overflow_error("Numeric truncation");
}
return static_cast<Target>(value);
}
void allocate_buffer_cpp(int64_t size) {
// Explicit validation and conversion
int32_t buffer_size = safe_narrow_cast<int32_t>(size);
if (buffer_size <= 0 || buffer_size > 10'000'000) {
throw std::invalid_argument("Invalid buffer size");
}
auto buffer = std::make_unique<char[]>(buffer_size);
}
// Better: use size_t throughout
void process_file_cpp(std::size_t file_size) {
// No conversion needed
auto buffer = std::make_unique<char[]>(file_size);
}
Why this works: std::numeric_limits provides portable type bounds. The template approach makes the validation reusable for any type pair. Throwing exceptions on truncation prevents silent data loss and forces proper error handling.
Exception-Based Validation (Java)
import java.math.BigDecimal;
public class SafeTruncation {
public static int safeLongToInt(long value) {
if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
throw new IllegalArgumentException(
"Value " + value + " cannot fit in int");
}
return (int) value;
}
public static byte[] allocateFromFileSize(long fileSize) {
int bufferSize = safeLongToInt(fileSize);
if (bufferSize <= 0 || bufferSize > 100_000_000) {
throw new IllegalArgumentException("Invalid buffer size");
}
return new byte[bufferSize];
}
// For financial calculations - avoid double
public static BigDecimal calculatePrice(BigDecimal amount,
BigDecimal rate) {
// No precision loss with BigDecimal
return amount.multiply(rate);
}
public static int safeDoubleToInt(double value) {
if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Value out of range");
}
if (Double.isNaN(value) || Double.isInfinite(value)) {
throw new IllegalArgumentException("Invalid value");
}
return (int) value;
}
}
Why this works: Java's explicit range checking prevents truncation errors. Using BigDecimal for financial calculations eliminates floating-point precision loss entirely. Checking for NaN and infinity prevents invalid conversions from special floating-point values.
Arbitrary Precision with Validation (Python)
import sys
def safe_int_conversion(value: int, max_value: int = sys.maxsize) -> int:
"""Validate integer fits in expected range."""
if value < 0 or value > max_value:
raise ValueError(f'Value {value} out of range [0, {max_value}]')
return value
def allocate_buffer(file_size: int) -> bytearray:
"""Allocate buffer from file size with validation."""
# Python ints are arbitrary precision, but validate practical limits
if file_size < 0:
raise ValueError('Size cannot be negative')
# Check against system limits
if file_size > 100_000_000: # 100MB
raise ValueError('File too large')
return bytearray(file_size)
Why this works: Python uses arbitrary precision integers, eliminating truncation in integer arithmetic. Validation ensures values stay within practical system limits (like available memory), preventing resource exhaustion while avoiding the truncation bugs common in fixed-width integer languages.
Security Checklist
- All narrowing conversions (64→32 bit) validate range first
- No implicit narrowing conversions (all use explicit casts)
- Timestamps use int64_t (not int) to avoid Y2038 bug
- Financial calculations avoid float (use double or integer cents)
- Compiler warnings enabled (-Wconversion, -Wnarrowing)
- Tests cover: normal values, MAX, MAX+1, MIN, MIN-1, truncation scenarios