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.,0x100000001for 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 -Werrorand confirm the build is clean; run Cppcheck or Clang Static Analyzer for additional coverage. Do not reach for-Wnarrowinghere: it is a C++ option, and GCC answers a C compilation carrying it withvalid for C++/ObjC++ but not for Crather than enabling anything.-Wconversionis what covers narrowing in C.
Common Pitfalls
- Fixing the range check but not the mismatched allocation/copy pair: validating the narrowed
buffer_sizecorrectly but then copying based on the original widefile_sizeelsewhere 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
-Wconversiononly 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).