Incorrect subtraction
| Vulnerability potential | High |
| DDoS potential | Low |
The subtraction is invalid
Impact
Subtracting a larger value from a smaller one in unsigned arithmetic does not produce a negative number — there are no negative unsigned values. The result wraps around to a huge positive number close to the type’s maximum. Code that expected a small or zero difference (a remaining length, a count, a gap) instead gets a value in the billions. When that value is used as a size, a loop bound, or an index, it drives the program far out of the intended range, typically into an out-of-bounds read or write.
Vulnerability potential
This issue has a strong potential to become a vulnerability (a form of CWE-191, integer underflow).
- Oversized length.
remaining = buffer_len - offsetunderflows whenoffset > buffer_len, yielding a near-SIZE_MAXvalue that is then passed tomemcpyor used as a copy bound, causing a massive out-of-bounds access. - Bypassed checks. A guard like
if (have - need >= 0)is always true for unsignedhave/need, so an attacker who makesneed > haveslips past it. - Negative loop counts.
for (size_t i = 0; i < count - 1; i++)withcount == 0loops aboutSIZE_MAXtimes, hanging the process or walking off the end of an array. - The wrong result can also crash the process, contributing to denial of service.
Technical details
Unsigned integer arithmetic in C and C++ is defined to be modular: every
operation is reduced modulo 2^N, where N is the width of the type. So for
unsigned a, b with a < b, the result of a - b is a - b + 2^N. This is
not undefined behavior — it is fully specified — which is exactly why it is
dangerous: there is no trap, no warning at runtime, just a quietly enormous
value.
Signed/unsigned mixing
The bug frequently appears through implicit conversion. In size_t len; int
delta; len - delta, the int is converted to size_t per the usual arithmetic
conversions, so a negative delta becomes huge before the subtraction even
happens. Comparisons are affected too: len - 1 >= 0 is always true because the
left side is unsigned.
Why the obvious check fails
if (a - b < 0) can never fire for unsigned operands, because an unsigned value
is never less than zero. The correct guard is to compare before subtracting:
if (a < b) / if (a >= b).
Catching the issue
Sanitizers
-fsanitize=unsigned-integer-overflow (Clang, and GCC) reports unsigned wraps,
including underflow, at runtime. It is not part of the default UBSan set because
intentional wrapping is legal, so enable it deliberately and whitelist code that
wraps on purpose.
Compiler warnings
-Wsign-conversion and -Wconversion catch the implicit signed-to-unsigned
conversions that hide most underflows. -Wtautological-compare flags
unsigned < 0 style checks that can never be true.
Static analysis
clang-tidy, Coverity, PVS-Studio, and CodeQL detect unsigned subtractions whose operands are not provably ordered, and “always true/false” unsigned comparisons.
Code review rule
Always test ordering before subtracting unsigned values: write
if (a >= b) diff = a - b; rather than subtracting first and checking the
result. Use ptrdiff_t/signed types when a difference legitimately needs a
sign.
How to reproduce
Observe that remaining becomes a huge value instead of a sensible small
number, and the memcpy would copy gigabytes.
#include <stdio.h>
#include <string.h>
int main(void)
{
size_t buffer_len = 16;
size_t offset = 32; /* offset past the end of the buffer */
size_t remaining = buffer_len - offset; /* underflows: ~SIZE_MAX */
printf("remaining = %zu\n", remaining); /* 18446744073709551600 */
/* memcpy(dst, src, remaining); would read/write far out of bounds. */
return 0;
}