Skip to content

CWE-197: Numeric Truncation Error - C

Overview

C narrows a value whenever it is assigned or cast to a smaller type - int64_t to int32_t, long to int, double to int - discarding whatever doesn't fit, with no runtime check and, without -Wconversion, often no compiler warning. Integer narrowing simply drops the high-order bits: 0x100000001 truncated to 32 bits becomes 1. Floating-point-to-integer narrowing is worse - converting a double outside the target integer type's range is undefined behavior in C, not just a loss of precision. A recurring real-world instance is the Y2038 problem: storing a 64-bit time_t in a 32-bit field, which overflows on January 19, 2038.

Common Vulnerable Patterns

64-bit to 32-bit Truncation Feeding an Allocation

#include <stdlib.h>
#include <stdint.h>

// VULNERABLE - 64-bit to 32-bit narrowing with no range check
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);   // allocates 1 byte instead of 4GB
    // any later code that copies based on file_size overflows this buffer
}

Why this is vulnerable: The implicit narrowing conversion from int64_t to int32_t keeps only the low 32 bits of file_size. An attacker who controls the reported file size can choose a value whose low 32 bits are small while the true size is enormous.

Double to Int Truncation

#include <math.h>

// VULNERABLE - double-to-int cast with no range check; out-of-range is undefined behavior
void process_calculation(double user_value) {
    // Attack: user_value = 2147483648.5 (just above INT_MAX)
    int result = user_value;   // undefined behavior - value is out of int's range

    if (result > 0) {
        char buffer[result];   // size is whatever undefined behavior produced
    }
}

Why this is vulnerable: Converting a double that doesn't fit in int's range is undefined behavior in C, not a defined wraparound - the compiler is free to produce any result, including one that looks superficially valid and passes the result > 0 check.

time_t Truncation (Y2038 Bug)

#include <time.h>

struct Event {
    int timestamp;   // 32-bit signed: max representable is January 19, 2038
};

// VULNERABLE - 64-bit time_t truncated into a 32-bit field (Y2038 bug)
void store_event(time_t event_time) {
    struct Event evt;
    evt.timestamp = event_time;   // truncates or overflows for dates past 2038
}

Why this is vulnerable: time_t is commonly 64-bit today, but int timestamp can only hold values up to 2147483647 (January 19, 2038, 03:14:07 UTC). Any later date silently produces the wrong stored value.

Secure Patterns

Range-Validated Narrowing Conversion

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.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;
    if (!safe_int64_to_int32(file_size, &buffer_size) || buffer_size <= 0) {
        fprintf(stderr, "Invalid or unrepresentable file size\n");
        return;
    }

    char *buffer = malloc((size_t)buffer_size);
    if (buffer != NULL) {
        free(buffer);
    }
}

Why this works: Checking value < INT32_MIN || value > INT32_MAX on the wide int64_t value, before the cast, catches exactly the values whose high-order bits would otherwise be silently dropped.

Validated Float-to-Int Conversion

#include <limits.h>
#include <math.h>
#include <stdbool.h>

bool safe_double_to_int(double value, int *result) {
    if (isnan(value) || isinf(value) || value < INT_MIN || value > INT_MAX) {
        return false;
    }
    *result = (int)value;
    return true;
}

Why this works: Rejecting NaN, infinity, and out-of-range values first means the cast only runs on a value known to be representable, so it never reaches the undefined behavior an out-of-range double-to-int conversion produces in C.

64-bit Timestamp Storage

#include <stdint.h>
#include <time.h>

struct Event {
    int64_t timestamp;   // matches time_t's typical width; doesn't overflow until year ~292 billion
};

void store_event_safe(time_t event_time) {
    struct Event evt;
    evt.timestamp = (int64_t)event_time;   // widening, not narrowing - always safe
}

Why this works: Storing the timestamp in a type at least as wide as time_t turns the conversion into a widening one, which cannot lose data. This avoids the Y2038 problem entirely rather than validating around it.

Considerations

Decide whether the narrowing is provably safe for this input domain. A conversion is fine when the source is already bounded - validated upstream, or structurally limited to a small range - and the question is whether that bound holds for every caller, not just the one in front of you. It usually does not hold for file sizes, lengths reported by a remote peer, aggregated totals, or dates far enough out to leave the 32-bit epoch. If you cannot point at where the value was bounded, treat it as unbounded.

Testing

  • Unit-test every conversion helper with the destination type's MIN, MAX, MAX + 1, and a source value whose low bits alone would look valid if truncated (e.g., 0x100000001 for a 32-bit destination).
  • For floating-point conversions, explicitly test NaN, +Infinity, -Infinity, and values just outside the integer range.
  • For date/time fields, test a value past January 19, 2038 to confirm the field's width actually accommodates it.
  • Compile with -Wconversion -Wfloat-conversion -Werror and confirm the build is clean; run Cppcheck or Clang Static Analyzer for additional coverage. Do not reach for -Wnarrowing here: it is a C++ option, and GCC answers a C compilation carrying it with valid for C++/ObjC++ but not for C rather than enabling anything. -Wconversion is what covers narrowing in C.

Common Pitfalls

  • Fixing the range check but not the mismatched allocation/copy pair: validating the narrowed buffer_size correctly but then copying based on the original wide file_size elsewhere reintroduces the overflow the check was meant to prevent.
  • Treating float-to-int truncation as "just losing the decimal part": for values outside the target integer's range, it is undefined behavior in C, not a predictable rounding-toward-zero.
  • Narrowing a timestamp "because 32 bits has always been enough": this is exactly the assumption behind the Y2038 bug - the fix is to store timestamps in a type wide enough from the start, not to patch every read site later.
  • Enabling -Wconversion only in new code: existing narrowing conversions elsewhere in the codebase stay silently unchecked unless the flag is applied build-wide (even if warnings are triaged incrementally).

Additional Resources