Skip to content

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

Additional Resources