Skip to content

CWE-197: Numeric Truncation Error

Overview

Numeric truncation occurs when a value is cast or assigned from a larger numeric type to a smaller one - long to int, int64 to int32, double to float or int - and the destination type cannot represent it. For integers the high-order bits are silently discarded; for floating point it is the fractional part or the high-magnitude part. Either way the result is a value unrelated to the original, not an error. Unlike CWE-195/CWE-196, which are specifically about crossing the signed/unsigned boundary, truncation is about crossing a width boundary and can happen entirely within signed types, entirely within unsigned types, or with floating point.

Relationship to Other CWEs

Risk

Medium-High: Truncation silently discards data: 0x100000001 narrowed to 32 bits becomes 1. When the truncated value drives a buffer allocation, the code can allocate space for the small truncated value while later code copies or writes based on the original, larger value - a classic setup for a buffer overflow. Truncation of time_t into a 32-bit field is the Y2038 problem. Truncation of financial values from double to float, or from floating point to integer, silently loses precision in ways that compound over repeated calculations.

Remediation Steps

Core Principle: Validate that a value fits in the destination type's range before narrowing it; never narrow implicitly.

Trace the Data Path

  • Source: Any wider-typed value (64-bit sizes/lengths, time_t, double results) that is assigned or cast to a narrower type
  • Sink: The narrowing assignment or cast itself - typically feeding a buffer size, an array index, a struct field, or a downstream calculation
  • Missing Controls: No check that the value is within [MIN, MAX] of the destination type before the narrowing conversion

Validate Range Before Narrowing (Primary Defense)

  • Compare the value against the destination type's MIN/MAX before casting
  • Make narrowing conversions explicit and validated, never implicit - the implicit ones are what the compiler warnings below are there to catch
  • For floating point, also reject NaN and infinity before converting to an integer type. In C and C++ an out-of-range conversion is undefined behaviour; in Java it is defined and therefore quieter, clamping to the type's MIN/MAX and turning NaN into 0, which is a wrong value the caller has no way to distinguish from a real one

Use a Wide Enough Type Instead of Narrowing (Defense in Depth)

  • Store timestamps, file sizes, and other values that can legitimately grow large in a 64-bit type from the start, rather than narrowing them later
  • Use a fixed-point or arbitrary-precision representation for financial calculations instead of float, and prefer double over float when floating point is unavoidable

Harden and Test

  • Enable compiler warnings for narrowing and conversion (-Wconversion -Wfloat-conversion on GCC/Clang, plus -Wnarrowing when the code is C++ rather than C, and /W4 on MSVC) and treat them as build failures. On MSVC these are C4244 for a narrowing integer conversion and C4267 for the size_t-to-smaller case that dominates this CWE's scenarios; both are on by default at /W4, so neither needs a flag of its own
  • Test every narrowing conversion path with the destination type's MIN, MAX, MAX + 1, and a value with high-order bits set
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
file_size = get_file_size()          // 64-bit value, e.g. 4GB + 1 byte
buffer_size = narrow_to_32_bit(file_size)   // high bits discarded -> becomes 1
buffer = allocate(buffer_size)        // allocates 1 byte
copy(buffer, file_size)               // copies based on the real, larger size - overflow
// Attack: a file (or any attacker-controlled length) sized so the narrowed value is small
// Result: an undersized allocation paired with a copy based on the true, larger size

Why this is vulnerable: narrowing keeps the low bits and discards the high ones with no signal that anything was dropped - no exception, no status flag, and no compiler warning by default. What comes out is a well-formed value of the destination type, which is the whole difficulty: 1 is a perfectly reasonable buffer size, and no check applied to buffer_size afterwards can tell it apart from a genuine 1.

The program is then holding two different lengths for the same object, and the attacker's work is to pick an input where they disagree usefully. The useful values do not look extreme: 0x100000001 narrows to 1 and 0x100000000 narrows to 0, so nothing in the request reads as an oversized one, and a validation rule that rejects implausibly large sizes lets it through.

Secure Patterns

// SECURE - pseudo-code
file_size = get_file_size()
if file_size > MAX_32_BIT:
    reject("file too large")
buffer_size = narrow_to_32_bit(file_size)   // safe: validated to fit
buffer = allocate(buffer_size)
copy(buffer, buffer_size)

Why this works: Checking the value against the destination type's maximum while it is still in the wider type catches anything that would lose bits, before the narrowing happens. Using the same validated buffer_size for both the allocation and the copy also removes the mismatch between "the size used to allocate" and "the size used to copy" that made the vulnerable version dangerous even before the truncation.

Common Pitfalls

  • Validating only the destination-type range after the value has already been narrowed: by then the information needed to detect the problem (the original, wide value) is gone - the check must happen on the wide value, before the cast.
  • Using the original wide value for a copy/write after allocating with the narrowed one: even with a valid narrowing conversion, allocating with buffer_size (narrow) and then copying file_size (wide) bytes reintroduces the overflow the narrowing check was supposed to prevent.
  • Treating narrowing as safe because "the values are usually small": a truncation bug is invisible in testing until an input crosses the destination type's boundary - which is exactly the case an attacker controlling the input will look for.
  • Storing a wide-range value (timestamps, file sizes) in a narrow field from the start "to save space": this defers the truncation to every future read/write of that field instead of avoiding it, and is how Y2038-style bugs get built in.

Language-Specific Guidance

  • C - explicit range checks before narrowing casts, Y2038-safe timestamp storage, compiler warning flags
  • C++ - a reusable safe_narrow_cast template built on std::in_range, and why the std::numeric_limits form of the same check is wrong
  • Java - explicit range validation for long-to-int and double-to-int narrowing, BigDecimal for financial values

Additional Resources