Skip to content

CWE-196: Unsigned to Signed Conversion Error

Overview

Unsigned to signed conversion errors occur when an unsigned value larger than the signed type's maximum is cast, assigned, or implicitly converted to a signed type. The high bit of the unsigned value's representation becomes the sign bit of the signed type, so the value arrives as a negative number instead of being rejected or clamped. How firmly that is guaranteed depends on the language: C++20 defines the conversion as reduction modulo 2^N, while C leaves it implementation-defined and permits a signal instead. Every mainstream compiler produces the negative value, and the remediation is the same either way - range-check before converting rather than reasoning about the result.

The conversion itself happens silently, with no runtime check, whenever an unsigned value flows into a context that expects a signed one, and that is not unique to C and C++ - Go, Rust and C#'s unchecked context all reinterpret the bits the same way. What is specific to C and C++ is what happens next: the resulting negative value reaches an allocation, a pointer expression or a memcpy with nothing between it and memory, so the mistake becomes corruption rather than an exception. That is why the guidance here is written for those two languages.

Relationship to Other CWEs

Risk

Medium-High: Converting a large unsigned value to signed produces a negative number (0xFFFFFFFF becomes -1), which breaks bounds checks, loop termination conditions, and arithmetic that assume the value can only be non-negative. A negative array index derived this way is an out-of-bounds access; a negative value fed to a size or count parameter can pass validation logic that only checks an upper bound. The most common source is strlen() or another size_t-returning call assigned into a plain int.

Remediation Steps

Core Principle: Validate that a value fits within the signed type's range before it is converted to, or used as, a signed type.

Trace the Data Path

  • Source: Unsigned values such as size_t lengths, strlen() results, array/buffer sizes, or values read from untrusted input into an unsigned type
  • Sink: The point where the unsigned value is cast or assigned to int or another signed type, or passed to a parameter declared signed - typically a loop bound, array size, or function argument. A mixed comparison is not this sink: the conversion rules convert an unsigned operand to signed only where the signed type can hold every value it might have, so a comparison that misbehaves on a negative operand is CWE-195
  • Missing Controls: No check that the unsigned value is <= <SignedType>_MAX before it crosses into signed territory

Validate the Range Before Converting (Primary Defense)

  • Compare the unsigned value against the destination signed type's maximum (INT_MAX, INT32_MAX) before casting or assigning
  • Do not blindly assign size_t results (strlen(), sizeof, container .size()) into int - check the range first
  • Where possible, keep the value unsigned throughout instead of converting it to signed at all

Keep Types Consistent (Defense in Depth)

  • Prefer one type family for a given quantity (all size_t, or a signed type validated to be large enough) rather than converting back and forth
  • Use ssize_t (POSIX) when a value legitimately needs to represent both a size and a negative error indicator

Harden and Test

  • Enable compiler warnings for narrowing and sign conversion (-Wsign-compare -Wsign-conversion -Wconversion on GCC/Clang, /W4 /w44365 on MSVC) and treat them as build failures. /W4 covers the comparison case (C4018) but not the conversion this page is about: C4365 is off by default at every warning level and has to be enabled by number
  • Test every conversion path with a value at, and one above, the destination type's maximum, plus SIZE_MAX
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
length = string_length(input)      // returns an unsigned size
count = (signed) length            // becomes negative if length exceeds the signed max
if count < limit:
    allocate(count)                // negative size - undefined or unexpected behavior
// Attack: input is large enough that its unsigned length exceeds the signed type's max
// Result: count is negative, silently passing checks that assume small positive values

Why this is vulnerable: the damage is done by a pair of conversions, not the one the sketch shows. count goes negative, passes a < limit check written for small positive numbers, and is then handed to an allocator whose size parameter is unsigned - so it converts straight back, and the small negative number becomes a value near the type's maximum. The code ends up requesting an enormous allocation by way of a check whose purpose was to keep it small.

This survives testing because the boundary is a size rather than a state. Every input below the signed type's maximum behaves correctly, and on a 32-bit int that is roughly 2.1 billion - so the defect is invisible until an input crosses it, and the party most likely to choose such an input is an attacker.

Secure Patterns

// SECURE - pseudo-code
length = string_length(input)
if length > SIGNED_MAX:
    reject("input too large")
count = (signed) length            // safe: length is now known to fit
if count > 0 and count < limit:
    allocate(count)

Why this works: Checking length > SIGNED_MAX while the value is still unsigned catches any value that would become negative when cast, before the conversion happens. Only a value confirmed to fit the signed range is ever converted.

Common Pitfalls

  • Trusting strlen()/.size() results in a plain int: these return size_t, which can exceed INT_MAX on a multi-gigabyte string or an attacker-controlled buffer - assigning without a range check silently produces a different length: negative up to 4 GB, and a small positive one above that, where int is the narrower type.
  • Checking the result after conversion instead of the source before it: count > 0 on the already-converted signed value does reject a wrapped negative, but only by treating a too-large input as if it were a malformed one. The shape that lets the value through is an upper bound with no lower bound - count < limit alone is satisfied by every negative number, which is exactly what the conversion produced.
  • Assuming "it will never be that big" without enforcing it: relying on typical input sizes staying under INT_MAX instead of validating it, which holds until an attacker or an unusually large file proves otherwise.
  • Converting to signed just to satisfy a function signature: casting an unsigned length to int to match a legacy API parameter without checking range first, when keeping the value unsigned (or updating the signature) would avoid the conversion entirely.

Language-Specific Guidance

  • C - range checks before casting to int, safe conversion helpers, compiler warning flags
  • C++ - std::numeric_limits-based bounds checks, exception-based conversion helpers

Additional Resources