The size_t
type is the unsigned integer type of the result of the sizeof
operator. Variables of type size_t
are guaranteed to be of sufficient precision to represent the size of an object. The limit of size_t
is specified by the SIZE_MAX
macro.
The type size_t
generally covers the entire address space. ISO/IEC TR 24731-1-2007 introduces a new type rsize_t
, defined to be size_t
but explicitly used to hold the size of a single object [[ISO/IEC TR 24731-1-2007]]. In code that documents this purpose by using the type rsize_t
, the size of an object can be checked to verify that it is no larger than RSIZE_MAX
, the maximum size of a normal single object, which provides additional input validation for library functions. See [STR07-A. Use TR 24731 for remediation of existing string manipulation code] for additional discussion of TR 24731-1.
Any variable that is used to represent the size of an object, including integer values used as sizes, indices, loop counters, and lengths, should be declared as rsize_t
if available, or otherwise as size_t
.
Non-Compliant Code Example
In this non-compliant code example, the dynamically allocated buffer referenced by p
overflows for values of n > INT_MAX
.
char *copy(size_t n, char const *str) { int i; char *p = (char *)malloc(n); if (p == NULL) { /* Handle malloc failure */ } for ( i = 0; i < n; ++i ) { p[i] = *str++; } return p; } char *p = copy(9, "hi there");
Signed integer overflow causes undefined behavior. The following is one possible scenario that shows why this should be avoided:
The size_t
type is typically represented by the same number of bits as int
, that is, sizeof(size_t) == sizeof(int))
. In this case, n
might be greater than INT_MAX
. Assuming quiet wraparound on signed overflow, the loop executes n
times because the comparison i < n
is an unsigned comparison. Once i > INT_MAX
, i
takes on negative values starting with (INT_MIN
). Consequently, the memory locations referenced by p[i]
precede the memory referenced by p
and a write-outside-array bounds occurs.
Under the same assumption, if size_t
is represented by a greater number of bits than int
, that is, sizeof(size_t) > sizeof(int)
, the same behavior occurs for values of n <= UINT_MAX
. For values of n > UINT_MAX
, all of memory within [INT_MIN, INT_MAX]
from the beginning of the output buffer is overwritten in an infinite loop. This is because the expression ++i
will wrap around to zero before the condition i < n
ever evaluates to false.
Note that in a preemptive multithreaded program, only one thread is in the infinite loop, so it is still significant that out-of-bounds memory is changed.
Compliant Solution
Declaring i
to be of type rsize_t
eliminates the possible integer overflow condition (in this example). Also, the argument n
is changed to be of type rsize_t
to document additional validation in the form of a check against RSIZE_MAX
.
char *copy(rsize_t n, char const *str) { rsize_t i; char *p; if (n > RSIZE_MAX) { /* Handle unreasonable object size error */ } p = (char *)malloc(n); if (p == NULL) { /* Handle malloc failure */ } for ( i = 0; i < n; ++i ) { p[i] = *str++; } return p; } char *p = copy(9, "hi there");
Non-Compliant Code Example
In this non-compliant code example, an integer overflow is specifically checked for by checking whether length + 1 == 0
(that is, integer wrap has occurred). If the test passes, a wrapper to malloc()
is called to allocate the appropriate data block (this is a common idiom). In a program compiled using an ILP32 compiler, this code runs as expected, but in an LP64 environment, an integer overflow can occur because length
is now a 64-bit value. The result of the expression, however, is truncated to 32 bits when passed as an argument to alloc()
because it takes an unsigned int
argument.
void *alloc(unsigned int blocksize) { return malloc(blocksize); } int read_counted_string(int fd) { unsigned long length; unsigned char *data; if (read_integer_from_network(fd, &length) < 0) { return -1; } if (length + 1 == 0) { /* handle integer overflow */ } data = (unsigned char*)alloc(length + 1); if (read_network_data(fd, data, length) < 0) { free(data); return -1; } data[length] = '\0'; /* ... */ free( data); return 0; }
Compliant Solution
Declaring both length
and the blocksize
argument to alloc()
as rsize_t
eliminates the possibility of truncation.
void *alloc(rsize_t blocksize) { if (blocksize > RSIZE_MAX) { /* Handle error */ } return malloc(blocksize); } int read_counted_string(int fd) { rsize_t length; unsigned char *data; if (read_integer_from_network(fd, &length) < 0) { return -1; } if (length + 1 == 0) { /* handle integer overflow */ } data = (unsigned char*)alloc(length + 1); if (read_network_data(fd, data, length) < 0) { free(data); return -1; } data[length] = '\0'; /* ... */ free( data); return 0; }
Risk Assessment
The improper calculation or manipulation of an object's size can result in exploitable vulnerabilities.
Recommendation |
Severity |
Likelihood |
Remediation Cost |
Priority |
Level |
---|---|---|---|---|---|
INT01-A |
medium |
probable |
medium |
P8 |
L2 |
Automated Detection
Fortify SCA Version 5.0 with CERT C Rule Pack will detect integer operations that cause overflow, but not all cases where size_t is not used.
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
References
[[ISO/IEC 9899:1999]] Section 7.17, "Common definitions <stddef.h>
", Section 7.20.3, "Memory management functions"
[[ISO/IEC TR 24731-1:2007]]
INT00-A. Understand the data model used by your implementation(s) 04. Integers (INT) INT02-A. Understand integer conversion rules