Zero Objects

Original author: tedu
  • Transfer
What is the difference between the following pairs of lengths and pointers?

size_t len1 = 0;
char *ptr1 = NULL;
size_t len2 = 0;
char *ptr2 = malloc(0);
size_t len3 = 0;
char *ptr3 = (char *)malloc(4096) + 4096;
size_t len4 = 0;
char ptr4[0];
size_t len5 = 0;
char ptr5[];


In many cases, all five expressions will lead to the same result. In others, their behavior can be radically different. One of the obvious differences is the ability to pass a pointer to free it, but we will not consider it.

The first case is interesting, but too different from the others, so for now we postpone it.

malloc (0)


The behavior of malloc (0) is defined by standards. You can return a null or unique pointer. The second option in many implementations is performed by an internal increase in length by one (which is then usually rounded to 16). According to the rules, you cannot dereference such a pointer, but usually a few bytes are nevertheless allocated, and therefore such a program will not crash.

Returning NULL leads to the possibility of an interesting bug. Often returning NULL from malloc is regarded as an error.

if ((ptr = malloc(len)) == NULL)
        err(1, "out of memory");


If len is zero, this will lead to an unlawful error message - if you do not add an additional check && len! = 0. You can also join the sect of adherents of “malloc uncheck”.

In OpenBSD, malloc treats zero differently. Placing data of size zero returns pieces of pages that have been protected via mprotect () with the key PROT_NONE. Attempting to dereference such a pointer will lead to a fall.

Note that the requirements for unique signs prohibit "cheating" when using them.

int thezero;
void *
malloc(size_t len)
{
        if (len == 0) return &thezero;
}
void
free(void *ptr)
{
        if (ptr == &thezero) return;
}


Such an implementation does not comply with the rules, since successive calls will return the same value. Therefore, the second case is similar to both the first and the third, depending on the implementation.

Other cases


If malloc does not throw an error, then options 3, 4, and 5 work identically in most cases. The main difference will be in the use of sizeof (ptr) / sizeof (ptr [0]), for example in a loop. This will lead to an incorrect answer, a correct answer, or will lead to nothing at all, breaking off at the compilation stage. Option 4 is not allowed by the standard, but compilers are likely to swallow it.

The biggest difference between these options from the first is that they pass a null check. It will be like the difference between an empty array and a missing array. And likewise, an empty string is not equal to a null string, although it occupies one byte in memory.

null objects


Let's go back to the first option and zero objects. Consider the following call:

memset(ptr, 0, 0);


We assign 0 bytes of ptr to 0. Which of the five listed pointers will make such a call? 3, 4 and 5. 2nd — if it is a unique pointer. But what if ptr is NULL?

Standard C in the section “Using Library Functions” says:
If the function argument has an invalid value (the value is outside the function domain, the pointer points to memory outside the area accessible to the program, or the pointer is null), then the behavior will not be determined.

The section "Conventions on string functions" specifies:
If the argument declared as size_t n determines the length of the array in the function, the value of n can be zero when this function is called. Unless the description of a particular function indicates otherwise, the values ​​of the argument arguments must be valid.

Apparently, a memset result of 0 bytes on NULL will be undefined. The documentation for memset, memcpy, and memmove does not indicate that they can accept null pointers. As a counterexample, we can describe snprintf, which says: "If n is zero, nothing is written, and s can be a null pointer." The documentation for the read function from POSIX similarly describes that reading zero length is not considered an error, but the implementation can check other parameters for errors - for example, invalid buffer pointers.

And what in practice? The easiest way to handle zero length in functions like memset or memcpy is to not enter the loop and do nothing. Usually in C, undefined behavior causes some kind of reaction, but in this case it has already been determined that nothing happens with normal pointers. Checking for pointer abnormalities would be superfluous.

Checking for non-zero, but invalid pointers is quite complicated. memcpy doesn't do this at all, letting the program just crash. The read call does not check anything either. It delegates a check to the copyout function, which sets up a handle to detect errors. Although you can add a null check, such pointers are no more invalid for these functions than 0x1 or 0xffffffff, for which there is no special processing.

Bummer


In practice, this means having a lot of code, implying (on purpose or by accident) that null pointers and zero length are valid. I decided to conduct an experiment by adding error output and output to memcpy if the pointer turned out to be NULL, and installed a new libc.

Feb 11 01:52:47 carbolite xsetroot: memcpy with NULL
Feb 11 01:53:18 carbolite last message repeated 15 times


Yeah, it didn't take long. I wonder what he does there:

Feb 11 01:53:18 carbolite gdb: memcpy with NULL
Feb 11 01:53:19 carbolite gdb: memcpy with NULL


Clearly understood. These messages seem to get bored very quickly. We return everything as it was.

Effects


I took up this issue, because at the intersection of areas “not defined, but should work” and optimization of C compilers is not doing anything good. A smart compiler can see the memcpy call, mark both pointers as valid, and remove null checks.

int backup;
void
copyint(int *ptr)
{
        size_t len = sizeof(int);
        if (!ptr)
                len = 0;
        memcpy(&backup, ptr, len);
}


But the code above, obviously, will not work as it should, if the compiler removes the check for zero and a null pointer is passed.

This question excites me, because in the past I have come across cases when such optimization of dereferences and checks led to security gaps. For software that is not ready for such a high level of compliance with standards, this is rather sad news.

At first, I could not convince the compiler to delete the check for zero after the “dereferencing” memcpy, but this does not mean that this cannot happen. gcc 4.9 says that this check will be removed by optimization. In OpenBSD, the gcc 4.9 package (containing many patches) does not delete the check by default, even with –O3, but if you enable “-fdelete-null-pointer-checks”, this removes the checks. I do not know what about clang - the first tests show that it does not delete, but there are no guarantees. In theory, he will also be able to carry out such an optimization.

Also popular now: