transaction blog
Oct 6, 2017 • 7 min read

When realloc() Doesn't Allocate

I recently wrote about correct error handling for malloc(). Coincidently I came across an older defect report on the behavior of C’s realloc() function just a few days ago. In this blog post, we’re going to look at realloc()’s behavior if it’s out of memory or if the requested size is zero.

A Simple realloc()

In C11, new memory blocks are dynamically allocated using one of malloc(), calloc(), or aligned_alloc().

If you read this blog post, you’ve probably seen calls to these functions frequently, including calls to realloc(). A call to realloc() changes the size of a memory block while preserving the block’s existing content. Here’s a naive implementation.

void*
realloc(void* old_mem, size_t new_size)
{
    if (!old_mem) {
        return malloc(new_size);
    } else if (!new_size) {
        return old_mem;
    }

    void* new_mem = malloc(new_size);
    if (!new_mem) {
        return NULL;
    }

    size_t old_size = ; // non-portable: get size of 'old_mem' from allocator

    memcpy(new_mem, old_mem, old_size < new_size ? old_size : new_size);
    free(old_mem);

    return new_mem;
}

What does it do? First of all, we get two corner cases out of the way. If no old memory buffer is specified, realloc() shall behave like malloc(). Our first condition handles this. Then we test if the new size is not zero. If it is zero, we return the old memory. C11 with DR 400 say that successful zero-size allocations shall behave as if the size were some nonzero value […]. We already know that old_mem contains a non-NULL value, so we return this. It’s not supposed to be dereferenced after this point, though.

With the corner cases handled, we can perform the real realloc(), which is malloc()-memcpy()-free() semantically. And finally we return the new buffer, which contains the content of the old buffer.

For the memcpy() operation, we need the size of the old buffer. There’s no such functionality standardized by ISO C or POSIX, but most allocators provide an interface for this: malloc_usable_size() in the GNU libc, BSD and Bionic, _msize() on Windows, or malloc_size() on MacOS.

Handling the Returned Value

Let’s now call realloc() and see what it does. One common programming mistake is illustrated below.

void* mem = malloc(10); /* non-zero allocation */
...

mem = realloc(mem, 20);
if (!mem) {
    /* handle allocation failure */
}

This pattern can often be found, although it is incorrect. What happens if the re-allocation fails? Of course, NULL is returned. If we immediately store realloc()’s returned value in mem, the old mem buffer leaks if realloc() fails. The correct code looks like this.

void* mem = malloc(10); // non-zero allocation
...

void* tmp = realloc(mem, 20);
if (!tmp) {
    /* handle allocation failure */
}
mem = tmp; /* Only update 'mem' is it's safe to do so */

Even if realloc() failed to allocate a new buffer, we still have our pointer to the old buffer and can free the old buffer later on.

Another, although less common, mistake is to access memory out-of-bounds after shrinking a buffer. Here’s an example.

char* mem = malloc(10); /* non-zero allocation */
...

void* tmp = realloc(mem, 5); /* shrink buffer */
if (!tmp) {
    /* handle allocation failure */
}
mem = tmp;

mem[7] = 0; /* This memory location is now outside the allocated buffer! */

Remember that semantically realloc() allocates a new buffer, copies the old buffer into the new buffer, and then frees the old buffer. Depending on the requested buffer size, the actual buffer size can also shrink. Accessing memory that was allocated with the old buffer, but not with the new buffer is therefore undefined behavior.

Finally, let’s see what happens if the requested new size is zero. Quoting DR 400 again:

  realloc(NULL, 0) realloc(non-NULL ptr, 0)
AIX always returns NULL, errno is EINVAL always returns NULL, frees ptr, errno is EINVAL
BSD returns NULL on error, errno is ENOMEM returns NULL on error, ptr unchanged, errno is ENOMEM
glibc returns NULL on error, errno is ENOMEM always returns NULL, ptr freed, errno unchanged

This is very inconsistent among older C implementations before C11. Every implementation behaves in a different way. I can’t bring myself to even try to write error-checking code that works correctly with all these variants. If you do, please send me some example code and I’ll post it here.

If we’re on C11, we’re lucky. For zero-size reallocations, DR 400 specifies that realloc() shall either return a NULL pointer to indicate an error, or that the old buffer remains in place. For all pre-C11 code that requires portability between different implementations, it’s best to write a wrapper around realloc() that handles the corner cases specifically.

void*
portable_realloc(void* mem, size_t siz)
{
    if (!mem) {
        return malloc(siz);
    } else if (!siz) {
        return mem;
    }
    return realloc(mem, siz);
}

Summary

In this blog post we looked at the corner cases of realloc().

If you like this blog post about memory allocation, please subscribe to the RSS feed, follow on Twitter or share on social networks.

Post by: Thomas Zimmermann


Subscribe to news feed