Skip to content

CWE-195: Signed to Unsigned Conversion Error

Overview

Signed to unsigned conversion errors occur when a negative signed integer is cast, assigned, or implicitly converted to an unsigned type. Because unsigned types have no sign bit, the negative value's two's complement bit pattern is reinterpreted as a huge positive number instead of being rejected.

Most languages perform this conversion silently, so silence is not what makes it a C and C++ problem. The difference is what happens next. In C and C++ the bogus length or index reaches an allocation, a pointer arithmetic expression, or memcpy with nothing checking the result, so the mistake becomes memory corruption. Where the runtime bounds-checks the access, the same conversion surfaces as an exception or a panic - a logic defect rather than a memory-safety one. That is why the guidance here is written for C and C++; for a managed language, the integer failure worth reading about is usually CWE-190 (Integer Overflow) or CWE-197 (Numeric Truncation).

Relationship to Other CWEs

Risk

High: Converting a negative signed value to unsigned produces a huge positive number (-1 becomes SIZE_MAX, roughly 18 quintillion on 64-bit systems). When that value reaches an allocation size, array index, loop bound, or memcpy length, it bypasses bounds checks that assume unsigned values are "obviously" non-negative, leading to massive over-allocations, out-of-bounds reads/writes, or crashes. This is a common consequence of treating a function's negative error return (read(), recv(), snprintf()) as if it were always a valid size.

Remediation Steps

Core Principle: Validate that a value is non-negative before it is converted to, compared with, or stored in an unsigned type.

Trace the Data Path

  • Source: Function return values that use a negative number as an error indicator (read(), recv(), snprintf()), user-controlled offsets or sizes, or any signed arithmetic result
  • Sink: The point where the signed value is cast to (or compared against) size_t or another unsigned type - typically a buffer size, array index, or allocation size
  • Missing Controls: No check that the signed value is >= 0 before it crosses into unsigned territory

Check for Negative Before Converting (Primary Defense)

  • Compare the signed value against 0 before assigning or casting it to an unsigned type
  • Check function return values for negative error codes before using them as sizes or indices
  • When comparing a signed and an unsigned value, validate the signed operand's sign first - the language implicitly converts it to unsigned before comparing, which can flip the result

Keep Types Consistent (Defense in Depth)

  • Prefer one type family for a given quantity throughout a function or module (all size_t, or a signed type large enough to validate safely) rather than converting back and forth
  • Use a dedicated signed-size type such as ssize_t on POSIX for values that can legitimately be either a negative error code or a valid non-negative size, so the negative case stays explicit instead of being silently absorbed

Harden and Test

  • Enable compiler warnings for sign conversion and comparison (-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
  • Run static analysis and test every conversion path with -1, INT_MIN, 0, and INT_MAX
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
result = read_from_source()        // returns -1 on error
count = (unsigned) result          // -1 becomes a huge positive number
if count > 0:
    allocate(count)                // huge or unexpected allocation
// Attack: the source call fails and returns -1
// Result: the unsigned count wraps to the type's maximum value, bypassing the "> 0" check

Why this is vulnerable: the sketch writes the conversion as an explicit cast, and real code almost never does. Assigning a signed result into an unsigned variable, passing a signed count to a parameter declared unsigned, or comparing the two all perform the same conversion silently, and compilers do not warn about it unless asked to. The line where the defect happens is often not a line anyone would think to look at.

The second thing the sketch understates is how little the attacker has to supply: nothing. The value that becomes enormous is the error return, so the attack is to make the read fail - close the connection early, point the code at a file it cannot open, exhaust a descriptor limit - and let the program's own failure path produce the huge size. Validation applied to the data never runs, because on this path no data ever arrived.

Secure Patterns

// SECURE - pseudo-code
result = read_from_source()
if result < 0:
    reject("read failed")
count = (unsigned) result          // safe: result is now known non-negative
if count > 0 and count <= buffer_capacity:
    allocate(count)

Why this works: Checking result < 0 while the value is still in its signed type catches the error case before the conversion happens. Once a negative value is confirmed impossible, casting the remaining non-negative range to unsigned is lossless and predictable.

Common Pitfalls

  • Checking after the cast instead of before: Casting to unsigned first, then checking count > 0 - the check always passes because a negative value already became a huge positive one during the cast, so the error case looks like a valid, enormous size.
  • Comparing signed and unsigned operands directly: if (signed_offset < unsigned_size) converts the signed operand to unsigned for the comparison, so a negative offset compares as larger than any real size. Written that way the guard answers false and skips its body, which is the safe direction - and is why the shape survives review. The arrangement that opens a hole is an upper-bound-only check evaluated while the value is still signed, if (length > (int)size) return -1;, which a negative length passes before converting to an enormous unsigned value at the copy or allocation below it. Check >= 0 in the signed domain whichever shape the guard has.
  • Trusting a function's purpose instead of its actual return type: Assuming a function that "returns a count" can't be negative, when its signature returns a signed type specifically so it can report an error via a negative value (read(), recv(), snprintf()).
  • Adding an upper bound without a lower one, after the conversion: Guarding the already-unsigned count with count < 10000 alone - a wrapped negative value is usually far above any reasonable upper bound too, so this often happens to catch -1, but it is incidental, not a real fix, and a different wrapped value or a different bound elsewhere in the codebase can still slip through.

Language-Specific Guidance

  • C - explicit non-negative checks before casting to size_t, safe conversion helpers, compiler warning flags
  • C++ - signed indices and lengths crossing into container size_type, std::ssize and std::cmp_*, why size() - 1 wraps on an empty container

Additional Resources