Skip to content

[SUGGESTION] Prevent implicit conversion of negative values when passed to unsigned parameters in certain functions #225

@bluetarpmedia

Description

@bluetarpmedia

Suggestion

Cppfront could prevent implicit conversion of negative values that are passed to unsigned parameters in well-known, memory-related functions, with the goal of eliminating a certain class of security vulnerabilities (see below).

Details

Many memory-related functions have size/length/count parameters of type size_t (or similar), e.g.

(1) void* memcpy(void* dest, const void* src, std::size_t count);
(2) std::vector::reserve(size_type new_cap);
(3) std::string::resize(size_type count);
(4) LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

A frequent source of security vulnerabilities (see below) is when a negative signed value is passed as the argument and implicitly converted to a (very large) unsigned value.

Cppfront could have a built-in list of well-known, memory-related functions (memcpy, memset, memmove, malloc, calloc, std::vector members, std::string members, VirtualAlloc, VirtualAllocEx, etc) along with the position(s) of their size/length/count unsigned parameters.

Cppfront could substitute all arguments passed to these size parameters for these well-known functions with a helper function which tests (at runtime) if the value is negative, and if so, calls std::terminate.

For example, Cppfront would transform this Cpp2 code:

get_length: () -> int;  // some function returning signed value

other_process : HANDLE = get_other_process();
length := get_length();
buffer : *void = VirtualAllocEx(other_process, nullptr, length, MEM_COMMIT, PAGE_READWRITE);

into something like:

HANDLE other_process {get_other_process()}; 
void* buffer { VirtualAllocEx(other_process, nullptr, cpp2::safe_unsigned_argument<SIZE_T>(length), MEM_COMMIT, PAGE_READWRITE) }; 

where cpp2::safe_unsigned_argument is something like:

template <typename ResultT, typename T>
constexpr ResultT safe_unsigned_argument(T t)
{
    if constexpr (std::is_unsigned_v<T>) {
        return static_cast<ResultT>(t);
    }

    if (t < 0) {
      assert(!"Negative value will be unsafely converted to an unsigned value.");
      std::terminate();
    }

    return static_cast<ResultT>(t);
}

This will require each documented parameter in the well-known memory functions to have their size type also documented, for use in the template return type ResultT. E.g.

safe_unsigned_argument<size_t>(length)                    // for memcpy
safe_unsigned_argument<std::vector::size_type>(length)    // for std::vector::resize / reserve
safe_unsigned_argument<SIZE_T>(length)                    // for VirtualAlloc

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?

Yes, this feature will eliminate security vulnerabilities caused by a negative signed value being implicitly converted to unsigned when passed to the size/length/count parameter of well-known (and frequently-used) memory functions. These vulnerabilities can cause stack overflow, buffer overflow and/or denial of service.

Example 4 in CWE-195: Signed to Unsigned Conversion Error has an example of the type of security vulnerability:

char* processNext(char* strm) {
  char buf[512];
  short len = *(short*) strm;   // <-- Untrusted user-input; may be negative
  strm += sizeof(len);
  if (len <= 512) {             // <-- Signed comparison (no warnings); branch is taken with negative `len`
    memcpy(buf, strm, len);     // <-- `len` implicitly converted to size_t, negative value becomes
    process(buf);               //       very large unsigned value causing stack overflow of `buf`
    return strm + len;
  }
  else {
    return -1;
  }
}

See also Example 4 in CWE-131: Incorrect Calculation of Buffer Size.

These CVEs are examples of security vulnerabilities that this feature would eliminate:

  • CVE-2021-33316 -- buffer overflow or invalid memory access caused by negative value passed to memcpy
  • CVE-2021-33315 -- as above
  • CVE-2016-8729 -- exploitable memory corruption and code execution caused by negative value passed to memset
  • CVE-2021-37669 -- denial of service caused by negative value passed to std::vector::resize
  • CVE-2021-37661 -- denial of service caused by negative value passed to std::vector::reserve
  • There are many more CVEs that appear with a simple search of memcpy negative or "memset" negative" but I haven't verified them since I felt the above was sufficient.

Will your feature suggestion automate or eliminate X% of current C++ guidance literature?

Best practice guidance is to enable a high level of warnings in the compiler (and ideally to treat warnings as errors).
For example see:

However, in my tests this was not sufficient to produce a warning on the signed/unsigned conversion in all cases. Given this Cpp1 code:

int get_length();
int main()
{
    std::vector<unsigned char> source(100);
    std::vector<unsigned char> dest(100);
    int len = get_length();
    memcpy(dest.data(), source.data(), len);
}
  • GCC 12.2 warns on the len conversion with -Wsign-conversion but I couldn't find any other way to make it warn (e.g. not with -Wconversion, -Wextra or -Wall)
  • Clang 15 warns on the conversion with -Wsign-conversion (or -Wconversion)
  • MSVC 19.33 did not produce a warning even with /W4. This is because the relevant warning C4365 is off by default. The warning can be enabled with /W4 /w44365.

This feature will automate this best practice for all users even if they have not enabled the relevant warning in their compiler, which as per the above is not always obvious or trivial.

Describe alternatives you've considered

An alternative for the cpp2::safe_unsigned_argument helper would be to avoid an explicit return type:

template <typename T>
constexpr auto safe_unsigned_argument(T t)
{
    if constexpr (std::is_unsigned_v<T>) {
        return t;
    }

    if (t < 0) {
      assert(!"Negative value will be unsafely converted to an unsigned value.");
      std::terminate();
    }

    return std::make_unsigned_t<T>(t);
}

This would have the benefit that Cppfront would not need to document the type for each size/count/length parameter in the list of well-known functions. However, it seemed a better design to be explicit in this case.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions