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
- CWE-196 (this page) - an unsigned value converted to signed, so a large positive number becomes negative
- CWE-195 (Signed to Unsigned Conversion Error) - the mirror image: both are children of CWE-681 and represent the same underlying mistake in opposite directions
- CWE-681 (Incorrect Conversion between Numeric Types) - the shared parent. No page here
- CWE-197 (Numeric Truncation Error) - overlapping, and covering narrowing conversions more broadly
- CWE-192 (Integer Coercion Error) - the general case of any implicit integer conversion producing an unexpected value
- CWE-190 (Integer Overflow) and CWE-191 (Integer Underflow) - distinct from this page: there a value is wrong because of arithmetic, not because of a type conversion
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_tlengths,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
intor 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>_MAXbefore 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_tresults (strlen(),sizeof, container.size()) intoint- 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 -Wconversionon GCC/Clang,/W4 /w44365on MSVC) and treat them as build failures./W4covers 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 plainint: these returnsize_t, which can exceedINT_MAXon 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, whereintis the narrower type. - Checking the result after conversion instead of the source before it:
count > 0on 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 < limitalone 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_MAXinstead 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
intto 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
- CWE-196: Unsigned to Signed Conversion Error
- SEI CERT INT31-C: Ensure that integer conversions do not result in lost or misinterpreted data
- SEI CERT INT32-C: Ensure operations on signed integers do not overflow - the adjacent rule rather than a conversion one: it governs arithmetic on the signed value, which is what a bad conversion hands you
- MSVC Compiler Warning (level 4, off) C4365: signed/unsigned mismatch on conversion
- MSVC Compiler Warning (level 3) C4018: signed/unsigned mismatch on comparison