Skip to content

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, and std::numeric_limits<std::size_t>::max().
  • For code reachable from untrusted input, test with a container or string large enough to make .size()/.length() exceed INT_MAX where the platform allows it.
  • Compile with -Wsign-conversion -Wsign-compare -Werror (or /W4 /w44365 /WX on MSVC, since C4365 is off by default and /W4 alone 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_cast documents 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-compare warning 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 with static_cast<int>(container.size()), because that cast is the unsigned-to-signed conversion the warning never mentioned. Use std::cmp_* or a range check.
  • Catching the std::overflow_error too 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 the sizeToInt example) is still required.

Additional Resources