CWE-196: Unsigned to Signed Conversion Error - C++
Overview
C++ inherits C's implicit unsigned-to-signed conversion rules, so the same wraparound risk applies to size_t, std::size_t-returning container methods (.size(), .length()), and any other unsigned value assigned into a signed type. C++ adds better tools for handling it: std::numeric_limits for portable range checks, exceptions for making an out-of-range conversion impossible to silently ignore, and (in C++20) std::cmp_less/std::cmp_greater for comparing signed and unsigned integers correctly without a manual cast at all.
Common Vulnerable Patterns
Unchecked .size()/.length() to int
#include <string>
// VULNERABLE - no range check before the size_t-to-int cast
int get_length(const std::string& str) {
return static_cast<int>(str.length()); // length() returns size_t
}
Why this is vulnerable: std::string::length() and std::vector::size() return size_t, which is wider than int on a 64-bit target, so static_cast<int> keeps only the low 32 bits and reads them as signed. Above INT_MAX that gives a negative number up to 4 GB and a small positive one beyond it - 4 GB + 5 bytes becomes 5. The conversion is defined behaviour since C++20, which mandated two's complement and modulo conversion, and implementation-defined before it. Either way nothing indicates that the value changed, and the caller receives a length it can use as a length.
Unsigned Size Converted to int Before the Bounds Check
#include <cstddef>
#include <cstring>
#include <vector>
// VULNERABLE - the guard is evaluated in int, which cannot hold a large length
void copy_into(std::vector<char>& dst, const char* src, std::size_t src_len) {
int needed = static_cast<int>(src_len); // above INT_MAX this is no longer src_len
if (needed <= static_cast<int>(dst.size())) { // a shrunken `needed` passes
std::memcpy(dst.data(), src, src_len); // the copy uses the true, huge length
}
}
Why this is vulnerable: the guard and the copy are looking at two different
numbers. needed is the converted value, and for any src_len above INT_MAX
it is smaller than the length it came from - negative below 4 GB, a small
positive number above it - so it compares as smaller than any destination size
and the branch is taken. std::memcpy is then handed src_len itself, which was never converted
and is as large as it always was. Casting dst.size() to int as well - which
this code does - is what makes the comparison look careful: both operands now
have the same type, and it is the one type that cannot represent the length.
The check ends up self-consistent and disconnected from the buffer it guards.
Note the direction. Here an unsigned value shrank and passed a check; in the mirror case on CWE-195 a signed value becomes enormous and fails one. The wrap that opens a hole is the one that makes a value look smaller than it is.
Secure Patterns
Range Check via std::numeric_limits
#include <cstddef>
#include <limits>
#include <stdexcept>
#include <string>
class SafeConversion {
public:
static int sizeToInt(std::size_t value) {
if (value > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
throw std::overflow_error("Size too large for int");
}
return static_cast<int>(value);
}
// Prefer not converting at all when the caller can accept size_t
static std::size_t getLength(const std::string& str) {
return str.length();
}
};
Why this works: std::numeric_limits<int>::max() gives a portable, type-correct bound instead of a hardcoded literal, which keeps the check correct across platforms with different int widths. Throwing on overflow makes the failure impossible to ignore, unlike a silent wraparound.
Compare Without Converting (C++20)
#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <utility>
#include <vector>
inline constexpr int MAX_CHUNK = 1 << 20; // a limit that arrives from config as an int
void copy_into(std::vector<char>& dst, const char* src, std::size_t src_len) {
if (src_len > dst.size() || std::cmp_greater(src_len, MAX_CHUNK)) {
throw std::length_error("source does not fit in destination");
}
std::memcpy(dst.data(), src, src_len);
}
Why this works: src_len > dst.size() needs no help, because both operands
are already size_t - the fix for the vulnerable version is simply to stop
converting. The second test is the one that would have reintroduced the problem:
policy limits usually arrive as int, and writing static_cast<int>(src_len) >
MAX_CHUNK puts the length back in the type it does not fit. std::cmp_greater
(C++20, <utility>) compares the two by mathematical value without converting
either operand, so the test stays correct whatever type and sign the limit is
declared with.
Avoid the Conversion by Staying Unsigned
#include <vector>
void process(const std::vector<int>& data) {
// Iterate with the container's own size type instead of converting to int
for (std::vector<int>::size_type i = 0; i < data.size(); ++i) {
// use data[i]
}
}
Why this works: When nothing outside the function needs a signed value, keeping the loop counter's type matched to .size()'s return type removes the conversion - and the class of bug - entirely.
Testing
- Unit-test conversion helpers with a normal value,
std::numeric_limits<int>::max(), one above it, andstd::numeric_limits<std::size_t>::max(). - For code reachable from untrusted input, test with a container or string large enough to make
.size()/.length()exceedINT_MAXwhere the platform allows it. - Compile with
-Wsign-conversion -Wsign-compare -Werror(or/W4 /w44365 /WXon MSVC, since C4365 is off by default and/W4alone does not report the conversion) and confirm the build is clean. - Static analysis (Clang-Tidy's
bugprone-narrowing-conversions, Cppcheck, or a commercial SAST tool) should report no unresolved signed/unsigned findings.
Common Pitfalls
- Reaching for
static_cast<int>as if it validates the value:static_castdocuments the intent to convert but performs no range check on its own - it silently produces the same wrapped result as a C-style cast. - Answering a
-Wsign-comparewarning with a cast: the mixed comparison itself converts the signed operand to unsigned, which is CWE-195's direction rather than this page's. It becomes this page's the moment someone silences the warning withstatic_cast<int>(container.size()), because that cast is the unsigned-to-signed conversion the warning never mentioned. Usestd::cmp_*or a range check. - Catching the
std::overflow_errortoo broadly and swallowing it: a catch block that logs and continues with a default value can reintroduce the same unchecked-size problem the exception was meant to prevent. - Assuming
std::cmp_*is available: these require C++20; on an earlier standard, an explicit range check (as in thesizeToIntexample) is still required.